Compare commits

...

412 Commits
v0.5.0 ... main

Author SHA1 Message Date
dependabot[bot] 3fbff4c6ca
Bump jupiterVersion from 5.13.1 to 5.13.2 (#334)
Bumps `jupiterVersion` from 5.13.1 to 5.13.2.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.13.1 to 5.13.2
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.13.1 to 5.13.2
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.13.1 to 5.13.2
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-07-02 12:34:15 -05:00
dependabot[bot] 25e05a99a7
Bump com.nimbusds:nimbus-jose-jwt from 10.3 to 10.3.1 (#335)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.3 to 10.3.1.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.3.1..10.3)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-version: 10.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 16:23:51 -05:00
dependabot[bot] a6059bdaeb
Bump jupiterVersion from 5.13.0 to 5.13.1 (#333)
Bumps `jupiterVersion` from 5.13.0 to 5.13.1.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.13.0 to 5.13.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.13.0 to 5.13.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.13.0 to 5.13.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 11:48:30 -05:00
dependabot[bot] c00a4d09b6
Bump io.netty:netty-transport-native-kqueue (#332)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.2.1.Final to 4.2.2.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-version: 4.2.2.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 09:16:14 -05:00
Max Lambrecht 6d428b4494
Prepare release v0.8.12 (#331)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-06-05 12:59:30 -05:00
dependabot[bot] 30f5bcf688
Bump jupiterVersion from 5.12.2 to 5.13.0 (#330)
Bumps `jupiterVersion` from 5.12.2 to 5.13.0.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.12.2 to 5.13.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.2...r5.13.0)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.12.2 to 5.13.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.2...r5.13.0)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.12.2 to 5.13.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.2...r5.13.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 15:18:49 -05:00
dependabot[bot] 06a1f4f489
Bump grpcVersion from 1.72.0 to 1.73.0 (#327)
Bumps `grpcVersion` from 1.72.0 to 1.73.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-protobuf` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-stub` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-inprocess` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-testing` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-netty-shaded` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

Updates `io.grpc:grpc-netty` from 1.72.0 to 1.73.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 15:10:52 -05:00
Peter 0d5eeccfe4
sslNegotiation is needed to pickup SpiffeSslSocketFactory (#325)
* sslNegotiation is needed to pickup SpiffeSslSocketFactory

Signed-off-by: Peter <peter.gassner@outlook.com>
2025-05-21 08:10:31 -05:00
dependabot[bot] db84782365
Bump io.netty:netty-transport-native-kqueue (#321)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.2.0.Final to 4.2.1.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-version: 4.2.1.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-05-20 15:00:02 -05:00
dependabot[bot] eb6731f712
Bump jupiterVersion from 5.12.1 to 5.12.2 (#319)
Bumps `jupiterVersion` from 5.12.1 to 5.12.2.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.12.1 to 5.12.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.1...r5.12.2)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.12.1 to 5.12.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.1...r5.12.2)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.12.1 to 5.12.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.1...r5.12.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-version: 5.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-version: 5.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-version: 5.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-05-20 14:49:43 -05:00
dependabot[bot] 0e00b0141e
Bump com.nimbusds:nimbus-jose-jwt from 10.2 to 10.3 (#323)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.2 to 10.3.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.3..10.2)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-version: '10.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 12:04:00 -05:00
Max Lambrecht 58171162d1
Revert 'system-stubs-core' update (#322)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-05-19 14:16:14 -05:00
dependabot[bot] 51779e5deb
Bump uk.org.webcompere:system-stubs-core from 2.0.3 to 2.1.8 (#320)
Bumps [uk.org.webcompere:system-stubs-core](https://github.com/webcompere/system-stubs) from 2.0.3 to 2.1.8.
- [Release notes](https://github.com/webcompere/system-stubs/releases)
- [Changelog](https://github.com/webcompere/system-stubs/blob/main/History.md)
- [Commits](https://github.com/webcompere/system-stubs/compare/system-stubs-parent-2.0.3...system-stubs-parent-2.1.8)

---
updated-dependencies:
- dependency-name: uk.org.webcompere:system-stubs-core
  dependency-version: 2.1.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 15:36:44 -05:00
dependabot[bot] 7560dd6359
Bump grpcVersion from 1.71.0 to 1.72.0 (#318)
Bumps `grpcVersion` from 1.71.0 to 1.72.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-protobuf` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-stub` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-inprocess` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-testing` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-netty-shaded` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

Updates `io.grpc:grpc-netty` from 1.71.0 to 1.72.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 19:25:41 -05:00
dependabot[bot] d39df4c7ae
Bump com.google.protobuf:protobuf-gradle-plugin from 0.9.4 to 0.9.5 (#314)
Bumps [com.google.protobuf:protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin) from 0.9.4 to 0.9.5.
- [Release notes](https://github.com/google/protobuf-gradle-plugin/releases)
- [Commits](https://github.com/google/protobuf-gradle-plugin/compare/v0.9.4...v0.9.5)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-gradle-plugin
  dependency-version: 0.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-04-18 11:20:18 -05:00
dependabot[bot] 0442dc5134
Bump io.netty:netty-transport-native-kqueue (#315)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.119.Final to 4.2.0.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.119.Final...netty-4.2.0.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-version: 4.2.0.Final
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-04-18 11:16:33 -05:00
dependabot[bot] ad6e33f5b5
Bump com.nimbusds:nimbus-jose-jwt from 10.0.2 to 10.2 (#317)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.0.2 to 10.2.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.2..10.0.2)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-version: '10.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-18 11:12:54 -05:00
dependabot[bot] 673f439f5e
Bump org.projectlombok:lombok from 1.18.36 to 1.18.38 (#313)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.36 to 1.18.38.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.36...v1.18.38)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-version: 1.18.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-18 11:08:25 -05:00
dependabot[bot] e0ac998038
Bump jupiterVersion from 5.12.0 to 5.12.1 (#310)
Bumps `jupiterVersion` from 5.12.0 to 5.12.1.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.12.0 to 5.12.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.0...r5.12.1)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.12.0 to 5.12.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.0...r5.12.1)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.12.0 to 5.12.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.0...r5.12.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-15 09:12:39 -05:00
dependabot[bot] f8b4576f6d
Bump grpcVersion from 1.70.0 to 1.71.0 (#308)
Bumps `grpcVersion` from 1.70.0 to 1.71.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-protobuf` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-stub` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-inprocess` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-testing` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-netty-shaded` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

Updates `io.grpc:grpc-netty` from 1.70.0 to 1.71.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 15:19:44 -06:00
dependabot[bot] 1d6d4b7134
Bump io.netty:netty-transport-native-kqueue (#307)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.118.Final to 4.1.119.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-02-27 16:31:50 -06:00
dependabot[bot] 9e3f67f5c7
Bump com.nimbusds:nimbus-jose-jwt from 10.0.1 to 10.0.2 (#306)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.0.1 to 10.0.2.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.0.2..10.0.1)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-02-27 14:19:23 -06:00
dependabot[bot] fc6283ea32
Bump jupiterVersion from 5.11.4 to 5.12.0 (#305)
Bumps `jupiterVersion` from 5.11.4 to 5.12.0.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.11.4 to 5.12.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.4...r5.12.0)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.11.4 to 5.12.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.4...r5.12.0)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.11.4 to 5.12.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.4...r5.12.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 14:14:36 -06:00
dependabot[bot] a7d47d43db
Bump io.netty:netty-transport-native-kqueue (#303)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.117.Final to 4.1.118.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 08:20:56 -06:00
dependabot[bot] 2de2d3f044
Bump grpcVersion from 1.69.0 to 1.70.0 (#302)
Bumps `grpcVersion` from 1.69.0 to 1.70.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-protobuf` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-stub` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-inprocess` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-testing` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-netty-shaded` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

Updates `io.grpc:grpc-netty` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.69.0...v1.70.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-01-23 14:14:18 -06:00
dependabot[bot] b169de3dce
Bump io.netty:netty-transport-native-kqueue (#300)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.116.Final to 4.1.117.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.116.Final...netty-4.1.117.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-23 14:09:51 -06:00
dependabot[bot] 0b5e60c486
Bump com.nimbusds:nimbus-jose-jwt from 9.47 to 10.0.1 (#297)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.47 to 10.0.1.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.0.1..9.47)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2025-01-07 07:42:37 -06:00
dependabot[bot] d9bad06282
Bump io.netty:netty-transport-native-kqueue (#294)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.115.Final to 4.1.116.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.115.Final...netty-4.1.116.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 07:42:04 -06:00
dependabot[bot] e5beed3362
Bump jupiterVersion from 5.11.3 to 5.11.4 (#293)
Bumps `jupiterVersion` from 5.11.3 to 5.11.4.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.11.3 to 5.11.4
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.11.3 to 5.11.4
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.11.3 to 5.11.4
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 07:32:23 -06:00
dependabot[bot] d629678fe5
Bump grpcVersion from 1.68.1 to 1.69.0 (#292)
Bumps `grpcVersion` from 1.68.1 to 1.69.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-protobuf` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-stub` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-inprocess` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-testing` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-netty-shaded` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

Updates `io.grpc:grpc-netty` from 1.68.1 to 1.69.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.1...v1.69.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 16:03:45 -06:00
Max Lambrecht 4e3d0cf703
Prepare release v0.8.11 (#288)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-18 16:08:12 -06:00
Max Lambrecht 4f86219c18
Bump spire version in integration test (#287)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-18 16:07:49 -06:00
dependabot[bot] 12b1b438a2
Bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#286)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.34 to 1.18.36.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.34...v1.18.36)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 08:54:39 -06:00
dependabot[bot] 1894482bc0
Bump com.nimbusds:nimbus-jose-jwt from 9.45 to 9.47 (#285)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.45 to 9.47.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.47..9.45)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-18 08:49:57 -06:00
dependabot[bot] 83cd18af0c
Bump io.netty:netty-transport-native-kqueue (#281)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.114.Final to 4.1.115.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.114.Final...netty-4.1.115.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-18 08:49:26 -06:00
Alexander Dümont 0526ec5d3a
Fix CRLF newline removal in DER format logic (#284)
Signed-off-by: Alexander Dümont <alexander_duemont@web.de>
2024-11-18 08:48:49 -06:00
Max Lambrecht de8b12c70a
Prepare release v0.8.10 (#279)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-06 15:05:28 -06:00
dependabot[bot] dcd1f8e020
Bump com.nimbusds:nimbus-jose-jwt from 9.42 to 9.45 (#278)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.42 to 9.45.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.45..9.42)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-11-06 09:26:19 -06:00
dependabot[bot] aa654e43ba
Bump grpcVersion from 1.68.0 to 1.68.1 (#276)
Bumps `grpcVersion` from 1.68.0 to 1.68.1.

Updates `io.grpc:protoc-gen-grpc-java` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-protobuf` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-stub` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-inprocess` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-testing` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-netty-shaded` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

Updates `io.grpc:grpc-netty` from 1.68.0 to 1.68.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.68.0...v1.68.1)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 09:22:40 -06:00
dependabot[bot] cf7697b2aa
Bump com.nimbusds:nimbus-jose-jwt from 9.41.2 to 9.42 (#275)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.41.2 to 9.42.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/commits/tag/9.42)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 09:52:14 -05:00
dependabot[bot] adf1207b2f
Bump jupiterVersion from 5.11.2 to 5.11.3 (#273)
Bumps `jupiterVersion` from 5.11.2 to 5.11.3.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.11.2 to 5.11.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.11.2 to 5.11.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.11.2 to 5.11.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 10:55:50 -05:00
Max Lambrecht 0023bec0c6
Prepare Release v0.8.9 (#272)
* Prepare release v0.8.9

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

* Minor

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

---------

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-09 11:34:37 -05:00
Max Lambrecht 7f53871ce0
Bump com.google.protobuf:protoc to 3.25.5 (#271)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-08 12:58:04 -05:00
Max Lambrecht fcee57f94c
Prepare release v0.8.8 (#270)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-08 12:01:43 -05:00
Max Lambrecht 026f3c51e7
Add testParseJWKSWithEmptyKeysArray (#233)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-08 09:58:20 -05:00
Max Lambrecht 5ec96673c9
Update gradle version to 8.10.2 (#269)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-07 16:16:23 -07:00
dependabot[bot] 45c7785c1c
Bump jupiterVersion from 5.11.1 to 5.11.2 (#268)
Bumps `jupiterVersion` from 5.11.1 to 5.11.2.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.11.1 to 5.11.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.11.1 to 5.11.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.11.1 to 5.11.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:27:22 -05:00
dependabot[bot] a4eaade918
Bump uk.org.webcompere:system-stubs-core from 2.0.3 to 2.1.7 (#264)
* Bump uk.org.webcompere:system-stubs-core from 2.0.3 to 2.1.7

Bumps [uk.org.webcompere:system-stubs-core](https://github.com/webcompere/system-stubs) from 2.0.3 to 2.1.7.
- [Release notes](https://github.com/webcompere/system-stubs/releases)
- [Changelog](https://github.com/webcompere/system-stubs/blob/main/History.md)
- [Commits](https://github.com/webcompere/system-stubs/compare/system-stubs-parent-2.0.3...system-stubs-parent-2.1.7)

---
updated-dependencies:
- dependency-name: uk.org.webcompere:system-stubs-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Amend version update for test dependency

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-01 18:18:14 -05:00
dependabot[bot] d946a25230
Bump com.nimbusds:nimbus-jose-jwt from 9.41.1 to 9.41.2 (#266)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.41.1 to 9.41.2.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/commits)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-01 18:14:39 -05:00
dependabot[bot] 36e114bbb6
Bump io.netty:netty-transport-native-kqueue (#265)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.113.Final to 4.1.114.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.113.Final...netty-4.1.114.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-10-01 18:09:20 -05:00
dependabot[bot] 9e1b2f5cf1
Bump jupiterVersion from 5.11.0 to 5.11.1 (#263)
Bumps `jupiterVersion` from 5.11.0 to 5.11.1.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.11.0 to 5.11.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.0...r5.11.1)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.11.0 to 5.11.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.0...r5.11.1)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.11.0 to 5.11.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.0...r5.11.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 18:04:45 -05:00
dependabot[bot] 18947c887e
Bump grpcVersion from 1.66.0 to 1.68.0 (#262)
Bumps `grpcVersion` from 1.66.0 to 1.68.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-protobuf` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-stub` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-inprocess` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-testing` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-netty-shaded` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

Updates `io.grpc:grpc-netty` from 1.66.0 to 1.68.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/commits)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 11:41:39 -05:00
Max Lambrecht 03118e3f12
Prepare release v0.8.7 (#261)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-09-20 12:42:25 -07:00
dependabot[bot] 86e6e32add
Bump commons-cli:commons-cli from 1.8.0 to 1.9.0 (#258)
Bumps commons-cli:commons-cli from 1.8.0 to 1.9.0.

---
updated-dependencies:
- dependency-name: commons-cli:commons-cli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-09-20 14:14:27 -05:00
dependabot[bot] 126a4d90ac
Bump com.nimbusds:nimbus-jose-jwt from 9.40 to 9.41.1 (#259)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.40 to 9.41.1.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.41.1..9.40)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-09-20 14:14:12 -05:00
dependabot[bot] 44683c69c8
Bump io.netty:netty-transport-native-kqueue (#260)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.112.Final to 4.1.113.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.112.Final...netty-4.1.113.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 14:13:52 -05:00
dependabot[bot] f0a16ee86c
Bump jupiterVersion from 5.10.2 to 5.11.0 (#254)
Bumps `jupiterVersion` from 5.10.2 to 5.11.0.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.10.2 to 5.11.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.2...r5.11.0)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.10.2 to 5.11.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.2...r5.11.0)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.10.2 to 5.11.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.2...r5.11.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 23:49:13 -07:00
dependabot[bot] f097e3b7af
Bump commons-validator:commons-validator from 1.8.0 to 1.9.0 (#251)
Bumps commons-validator:commons-validator from 1.8.0 to 1.9.0.

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 23:40:20 -07:00
dependabot[bot] c9251b3339
Bump org.projectlombok:lombok from 1.18.32 to 1.18.34 (#253)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 23:34:31 -07:00
dependabot[bot] 987944188d
Bump org.apache.commons:commons-lang3 from 3.14.0 to 3.17.0 (#255)
Bumps org.apache.commons:commons-lang3 from 3.14.0 to 3.17.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 22:06:51 -07:00
dependabot[bot] b0c9de6c52
Bump io.netty:netty-transport-native-kqueue (#247)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.109.Final to 4.1.112.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.112.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 14:17:53 -05:00
dependabot[bot] 7e891811aa
Bump com.nimbusds:nimbus-jose-jwt from 9.37.3 to 9.40 (#244)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.37.3 to 9.40.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.40..9.37.3)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 14:17:17 -05:00
dependabot[bot] 966b5b3f4f
Bump commons-cli:commons-cli from 1.7.0 to 1.8.0 (#241)
Bumps commons-cli:commons-cli from 1.7.0 to 1.8.0.

---
updated-dependencies:
- dependency-name: commons-cli:commons-cli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 14:16:45 -05:00
dependabot[bot] b3d0f33e83
Bump docker/build-push-action from 5 to 6 (#246)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 14:16:16 -05:00
dependabot[bot] 46423ab3d7
Bump grpcVersion from 1.63.0 to 1.66.0 (#248)
Bumps `grpcVersion` from 1.63.0 to 1.66.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-protobuf` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-stub` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-inprocess` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-testing` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-netty-shaded` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

Updates `io.grpc:grpc-netty` from 1.63.0 to 1.66.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.63.0...v1.66.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 14:13:40 -05:00
Max Lambrecht b0ab86ba31
Update gradle version to 8.10 (#231)
* Update gradle version to 8.7

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

* Update gradle to 8.10

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

---------

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
2024-08-16 12:01:51 -07:00
Max Lambrecht bf552c41d3
Change CI macos jdk distribution to zulu (#249)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-08-16 11:47:08 -07:00
dependabot[bot] 68565d8813
Bump commons-cli:commons-cli from 1.6.0 to 1.7.0 (#235)
Bumps commons-cli:commons-cli from 1.6.0 to 1.7.0.

---
updated-dependencies:
- dependency-name: commons-cli:commons-cli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 17:30:23 -05:00
dependabot[bot] 7dfbffb8a3
Bump io.netty:netty-transport-native-kqueue (#234)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.108.Final to 4.1.109.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.108.Final...netty-4.1.109.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 16:24:38 -05:00
dependabot[bot] daa0bbae83
Bump grpcVersion from 1.62.2 to 1.63.0 (#232)
Bumps `grpcVersion` from 1.62.2 to 1.63.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-protobuf` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-stub` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-inprocess` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-testing` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-netty-shaded` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

Updates `io.grpc:grpc-netty` from 1.62.2 to 1.63.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 16:38:20 -05:00
dependabot[bot] 8af85e3f79
Bump io.netty:netty-transport-native-kqueue (#229)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-03-21 18:17:44 -05:00
dependabot[bot] 4cc83b9d80
Bump org.projectlombok:lombok from 1.18.30 to 1.18.32 (#228)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-21 18:01:47 -05:00
Max Lambrecht 78f2b722a2
Add Run Build Workflow on Push (#226)
Add Run Build Workflow on Push

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-03-08 18:15:02 -08:00
Max Lambrecht 4649f50cf3
Update spire version used in integration tests to 1.9.1 (#225)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-03-08 12:20:17 -06:00
Max Lambrecht 7bbe3ed36b
Prepare Release 0.8.6 (#224)
Prepare Release 0.8.6

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-03-04 16:17:49 -06:00
Moritz Schmitz von Hülst 616e8bc3d1
Add functional test for java-spiffe-helper (#207)
Add functional test for java-spiffe-helper

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Only run on pull_request

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* No need for selector, there is only one pod

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Implement suggested changes

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Use tee for logs

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* A few changes according to comments

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Extend workflow to support multiple semver ranges for SPIRE helm charts

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Extend workflow to support multiple semver ranges for SPIRE helm charts

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* USe SemVer range instead of fixed version

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

---------

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-03-01 08:43:32 -06:00
Max Lambrecht c5629c58e7
Update PR Build Workflow (#217)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-28 15:31:02 -06:00
Max Lambrecht 803dea9a91
Automate and Refactor Release Build (#220)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-28 14:36:36 -06:00
dependabot[bot] f8a146b53e
Bump uk.org.webcompere:system-stubs-core from 2.0.2 to 2.1.6 (#191)
* Bump uk.org.webcompere:system-stubs-core from 2.0.2 to 2.1.6

Bumps [uk.org.webcompere:system-stubs-core](https://github.com/webcompere/system-stubs) from 2.0.2 to 2.1.6.
- [Release notes](https://github.com/webcompere/system-stubs/releases)
- [Changelog](https://github.com/webcompere/system-stubs/blob/main/History.md)
- [Commits](https://github.com/webcompere/system-stubs/compare/system-stubs-parent-2.0.2...system-stubs-parent-2.1.6)

---
updated-dependencies:
- dependency-name: uk.org.webcompere:system-stubs-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Conditional dependecy uk.org.webcompere version based on java version

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-28 14:30:03 -06:00
Max Lambrecht 4103c93262
Point badge to main branch (#219)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-28 12:48:39 -06:00
dependabot[bot] 6e993c4350
Bump grpcVersion from 1.61.1 to 1.62.2 (#222)
Bumps `grpcVersion` from 1.61.1 to 1.62.2.

Updates `io.grpc:protoc-gen-grpc-java` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-protobuf` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-stub` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-inprocess` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-testing` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-netty-shaded` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

Updates `io.grpc:grpc-netty` from 1.61.1 to 1.62.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.1...v1.62.2)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 16:51:23 -06:00
dependabot[bot] 8c9a6d1333
Bump com.google.protobuf:protoc from 3.25.2 to 3.25.3 (#218)
Bumps [com.google.protobuf:protoc](https://github.com/protocolbuffers/protobuf) from 3.25.2 to 3.25.3.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.25.2...v3.25.3)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protoc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 16:32:19 -06:00
Max Lambrecht 8bc61109db
Revert the addition of 'v' to docker image tags (#216)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 15:37:41 -06:00
Max Lambrecht 9215056b63
Fix Gradle Deprecation Warnings (#211)
Fix Gradle warnings

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 15:37:23 -06:00
Max Lambrecht 881dbd0bf8
Use Gradle Wrapper in Dockerfile (#213)
Use Gradle wrapper in Dockerfile

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 14:58:09 -06:00
Max Lambrecht 15d2980d74
Upgrade Gradle version to 8.6 (#214)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 14:57:35 -06:00
Max Lambrecht b19bd407b4
Fix jacoco config (#212)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 14:56:58 -06:00
Max Lambrecht bc833fef25
Update SPIRE version used in integration tests (#215)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-16 14:50:51 -06:00
Max Lambrecht 9b8b7dad87
Fix image url in the README (#209)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 17:09:16 -06:00
Max Lambrecht 0ff26093f4
Include 'v' in the image tag (#210)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 17:08:58 -06:00
dependabot[bot] 07285eb5e9
Bump io.netty:netty-transport-native-kqueue (#205)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.106.Final to 4.1.107.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.106.Final...netty-4.1.107.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 16:15:01 -06:00
Max Lambrecht 63a004d9f3
Prepare release 0.8.5 (#204)
Co-authored-by: Ryan Turner <turner@uber.com>
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 14:11:06 -06:00
Max Lambrecht 6a8e96ba96
Fix CODEOWNERS syntax (#208)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 13:22:48 -06:00
Max Lambrecht 4d374bf798
DefineC grpc-inprocess dependency as testImplementation (#206)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-15 13:20:55 -06:00
dependabot[bot] 93da062279
Bump jupiterVersion from 5.10.1 to 5.10.2 (#203)
Bumps `jupiterVersion` from 5.10.1 to 5.10.2.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.10.1 to 5.10.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.1...r5.10.2)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.10.1 to 5.10.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.1...r5.10.2)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.10.1 to 5.10.2
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.1...r5.10.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 14:57:01 -06:00
Moritz Schmitz von Hülst 27b2a01712
Add docker build for java-spiffe-helper container (#187)
* Add docker build for java-spiffe-helper container

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Adopt JDK has been deprecated in favor of temurin

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Push image after build

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Remove build on pull request and fix image tag

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Set user non-root, add separate stage for gradle dependencies and version to gradle properties

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Fix entrypoint

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Add example config and default container command

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Revert changes to gradle workflow and rename container build workflow

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Pin gradle builder image version

Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Add buildx action

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Use github variable in image tag

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Add Qemu

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Login before push

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Adopt JDK has been deprecated in favor of temurin

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Remove build on pull request and fix image tag

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Revert changes to gradle workflow and rename container build workflow

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Use new properties example file

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

* Minor improvements according to PR comments

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>

---------

Signed-off-by: Moritz Schmitz von Hülst <moritz.schmitz@oviva.com>
Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
2024-02-05 15:51:08 -06:00
Max Lambrecht 666766a90f
Enhancements: Example Config File, Default Path, and Error Handling Improvements (#199)
Add default helper config
Improving java-spiffe-helper Runner and Config logic
Improve error handling in java-spiffe-helper
Update README

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-02-05 09:13:31 -06:00
dependabot[bot] 56bb734de3
Bump grpcVersion from 1.61.0 to 1.61.1 (#202)
Bumps `grpcVersion` from 1.61.0 to 1.61.1.

Updates `io.grpc:protoc-gen-grpc-java` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-protobuf` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-stub` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-inprocess` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-testing` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-netty-shaded` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

Updates `io.grpc:grpc-netty` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.61.0...v1.61.1)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-inprocess
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 09:07:06 -06:00
Ryan Turner 57c3634f06
Update to Gradle 8.5 (#201)
Signed-off-by: Ryan Turner <turner@uber.com>
2024-01-31 06:56:10 -06:00
dependabot[bot] 71eecdfefa
Bump commons-validator:commons-validator from 1.7 to 1.8.0 (#197)
Bumps commons-validator:commons-validator from 1.7 to 1.8.0.

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-25 17:14:07 -06:00
dependabot[bot] 3730421a32
Bump commons-cli:commons-cli from 1.5.0 to 1.6.0 (#196)
Bumps commons-cli:commons-cli from 1.5.0 to 1.6.0.

---
updated-dependencies:
- dependency-name: commons-cli:commons-cli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-25 17:13:49 -06:00
dependabot[bot] 2a97a32493
Bump jupiterVersion from 5.10.0 to 5.10.1 (#195)
Bumps `jupiterVersion` from 5.10.0 to 5.10.1.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-25 17:13:11 -06:00
Max Lambrecht 77f3893dd5
Add mergeServiceFiles to shawdowJar configs (#198)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-24 17:56:37 -06:00
dependabot[bot] c59c5d2c77
Bump com.google.protobuf:protoc from 3.23.4 to 3.25.2 (#193)
Bumps [com.google.protobuf:protoc](https://github.com/protocolbuffers/protobuf) from 3.23.4 to 3.25.2.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.23.4...v3.25.2)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protoc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-24 09:31:04 -06:00
dependabot[bot] 9762799dcf
Bump grpcVersion from 1.58.0 to 1.61.0 (#190)
* Bump grpcVersion from 1.58.0 to 1.61.0

Bumps `grpcVersion` from 1.58.0 to 1.61.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

Updates `io.grpc:grpc-protobuf` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

Updates `io.grpc:grpc-stub` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

Updates `io.grpc:grpc-testing` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

Updates `io.grpc:grpc-netty-shaded` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

Updates `io.grpc:grpc-netty` from 1.58.0 to 1.61.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.58.0...v1.61.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add missing dependency for grpc 1.61.0

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-24 09:25:38 -06:00
dependabot[bot] acdbd0696a
Bump io.netty:netty-transport-native-kqueue (#192)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.97.Final to 4.1.106.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.97.Final...netty-4.1.106.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-24 07:32:10 -06:00
dependabot[bot] 69628d2eb6
Bump org.apache.commons:commons-lang3 from 3.13.0 to 3.14.0 (#189)
Bumps org.apache.commons:commons-lang3 from 3.13.0 to 3.14.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 07:24:28 -06:00
dependabot[bot] a073d66c5a
Bump actions/cache from 3 to 4 (#188)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 13:58:35 -08:00
dependabot[bot] fec770ec95
Bump com.nimbusds:nimbus-jose-jwt from 9.35 to 9.37.3 (#184)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.35 to 9.37.3.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.37.3..9.35)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-22 06:52:09 -06:00
dependabot[bot] d318bc5b3c
Bump actions/setup-java from 3 to 4 (#182)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2024-01-22 06:50:32 -06:00
dependabot[bot] 84ad698571
Bump org.projectlombok:lombok from 1.18.28 to 1.18.30 (#170)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.28 to 1.18.30.
- [Release notes](https://github.com/projectlombok/lombok/releases)
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.28...v1.18.30)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 06:49:56 -06:00
dependabot[bot] c3b58d12c3
Bump com.nimbusds:nimbus-jose-jwt from 9.34 to 9.35 (#169)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.34 to 9.35.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.35..9.34)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 11:29:41 -05:00
dependabot[bot] 9a356a8757
Bump com.nimbusds:nimbus-jose-jwt from 9.31 to 9.34 (#168)
Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.31 to 9.34.
- [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.34..9.31)

---
updated-dependencies:
- dependency-name: com.nimbusds:nimbus-jose-jwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-09-15 12:29:37 -05:00
dependabot[bot] a73726af97
Bump grpcVersion from 1.57.2 to 1.58.0 (#165)
Bumps `grpcVersion` from 1.57.2 to 1.58.0.

Updates `io.grpc:protoc-gen-grpc-java` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

Updates `io.grpc:grpc-protobuf` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

Updates `io.grpc:grpc-stub` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

Updates `io.grpc:grpc-testing` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

Updates `io.grpc:grpc-netty-shaded` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

Updates `io.grpc:grpc-netty` from 1.57.2 to 1.58.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.57.2...v1.58.0)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-09-15 12:23:48 -05:00
dependabot[bot] 99a03ff33d
Bump actions/checkout from 3 to 4 (#163)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 12:23:13 -05:00
dependabot[bot] 0c8b71642e
Bump io.netty:netty-transport-native-kqueue (#162)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.96.Final to 4.1.97.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.96.Final...netty-4.1.97.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 13:21:38 -05:00
dependabot[bot] 817709c82f
Bump grpcVersion from 1.56.1 to 1.57.2 (#161)
Bumps `grpcVersion` from 1.56.1 to 1.57.2.

Updates `io.grpc:protoc-gen-grpc-java` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

Updates `io.grpc:grpc-protobuf` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

Updates `io.grpc:grpc-stub` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

Updates `io.grpc:grpc-testing` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

Updates `io.grpc:grpc-netty-shaded` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

Updates `io.grpc:grpc-netty` from 1.56.1 to 1.57.2
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.56.1...v1.57.2)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-08-17 09:51:03 -05:00
dependabot[bot] c9f01b09bf
Bump io.netty:netty-transport-native-kqueue (#156)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.94.Final to 4.1.96.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.94.Final...netty-4.1.96.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 09:44:49 -05:00
dependabot[bot] 0246dfb224
Bump jupiterVersion from 5.9.3 to 5.10.0 (#155)
Bumps `jupiterVersion` from 5.9.3 to 5.10.0.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.9.3 to 5.10.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.9.3 to 5.10.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.9.3 to 5.10.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 12:22:24 -07:00
dependabot[bot] 659145cc3e
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0 (#158)
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 11:53:38 -07:00
dependabot[bot] ad06ea59af
Bump com.google.protobuf:protobuf-gradle-plugin from 0.9.3 to 0.9.4 (#153)
Bumps [com.google.protobuf:protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin) from 0.9.3 to 0.9.4.
- [Release notes](https://github.com/google/protobuf-gradle-plugin/releases)
- [Commits](https://github.com/google/protobuf-gradle-plugin/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 11:48:10 -07:00
dependabot[bot] e4e7f8dace
Bump com.google.protobuf:protoc from 3.23.1 to 3.23.4 (#152)
Bumps [com.google.protobuf:protoc](https://github.com/protocolbuffers/protobuf) from 3.23.1 to 3.23.4.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.23.1...v3.23.4)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protoc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-07-11 15:21:46 -05:00
dependabot[bot] 3f9e328936
Bump grpcVersion from 1.55.1 to 1.56.1 (#151)
Bumps `grpcVersion` from 1.55.1 to 1.56.1.

Updates `io.grpc:protoc-gen-grpc-java` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

Updates `io.grpc:grpc-protobuf` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

Updates `io.grpc:grpc-stub` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

Updates `io.grpc:grpc-testing` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

Updates `io.grpc:grpc-netty-shaded` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

Updates `io.grpc:grpc-netty` from 1.55.1 to 1.56.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.55.1...v1.56.1)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-07-11 15:17:27 -05:00
dependabot[bot] bc7f6703fa
Bump io.netty:netty-transport-native-kqueue (#150)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.92.Final to 4.1.94.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.92.Final...netty-4.1.94.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 15:12:11 -05:00
dependabot[bot] 67f0174b11
Bump org.projectlombok:lombok from 1.18.26 to 1.18.28 (#145)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.26 to 1.18.28.
- [Release notes](https://github.com/projectlombok/lombok/releases)
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.26...v1.18.28)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 21:42:02 -05:00
dependabot[bot] d18fa737d0
Bump com.google.protobuf:protoc from 3.23.0 to 3.23.1 (#144)
Bumps [com.google.protobuf:protoc](https://github.com/protocolbuffers/protobuf) from 3.23.0 to 3.23.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.23.0...v3.23.1)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protoc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 18:10:22 -05:00
dependabot[bot] b925e5ecf1
Bump com.google.protobuf:protoc from 3.21.12 to 3.23.0 (#142)
Bumps [com.google.protobuf:protoc](https://github.com/protocolbuffers/protobuf) from 3.21.12 to 3.23.0.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.21.12...v3.23.0)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protoc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-10 10:37:48 -05:00
dependabot[bot] 67a34e986c
Bump grpcVersion from 1.54.1 to 1.55.1 (#143)
Bumps `grpcVersion` from 1.54.1 to 1.55.1.

Updates `io.grpc:protoc-gen-grpc-java` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

Updates `io.grpc:grpc-protobuf` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

Updates `io.grpc:grpc-stub` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

Updates `io.grpc:grpc-testing` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

Updates `io.grpc:grpc-netty-shaded` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

Updates `io.grpc:grpc-netty` from 1.54.1 to 1.55.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.1...v1.55.1)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-10 10:33:40 -05:00
dependabot[bot] 3e1a50e5f8
Bump com.google.protobuf:protobuf-gradle-plugin from 0.9.2 to 0.9.3 (#141)
Bumps [com.google.protobuf:protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin) from 0.9.2 to 0.9.3.
- [Release notes](https://github.com/google/protobuf-gradle-plugin/releases)
- [Commits](https://github.com/google/protobuf-gradle-plugin/compare/0.9.2...v0.9.3)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 11:54:28 -05:00
Max Lambrecht 73404a80ad
Upgrade gradle version to 8.1.1 (#138)
* Update Gradle version to 8.1.1

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2023-04-27 11:36:01 -07:00
dependabot[bot] a0480f91d9
Bump jupiterVersion from 5.9.2 to 5.9.3 (#140)
Bumps `jupiterVersion` from 5.9.2 to 5.9.3.

Updates `org.junit.jupiter:junit-jupiter-api` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.2...r5.9.3)

Updates `org.junit.jupiter:junit-jupiter-engine` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.2...r5.9.3)

Updates `org.junit.jupiter:junit-jupiter-params` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.2...r5.9.3)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.junit.jupiter:junit-jupiter-params
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-27 13:25:37 -05:00
dependabot[bot] f3d360ba7d
Bump io.netty:netty-transport-native-kqueue (#139)
Bumps [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) from 4.1.91.Final to 4.1.92.Final.
- [Release notes](https://github.com/netty/netty/releases)
- [Commits](https://github.com/netty/netty/compare/netty-4.1.91.Final...netty-4.1.92.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-transport-native-kqueue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-27 13:24:57 -05:00
dependabot[bot] cb9baa85c6
Bump grpcVersion from 1.54.0 to 1.54.1 (#136)
Bumps `grpcVersion` from 1.54.0 to 1.54.1.

Updates `io.grpc:protoc-gen-grpc-java` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

Updates `io.grpc:grpc-protobuf` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

Updates `io.grpc:grpc-stub` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

Updates `io.grpc:grpc-testing` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

Updates `io.grpc:grpc-netty-shaded` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

Updates `io.grpc:grpc-netty` from 1.54.0 to 1.54.1
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.54.0...v1.54.1)

---
updated-dependencies:
- dependency-name: io.grpc:protoc-gen-grpc-java
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-stub
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-testing
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.grpc:grpc-netty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-18 09:23:21 -05:00
Max Lambrecht 537e7f7a7b
Prepare release 0.8.4 (#134)
Prepare release 0.8.4

Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-14 13:47:26 -05:00
dependabot[bot] 70ccecd600
Bump mockitoVersion from 3.12.4 to 4.11.0 (#133)
* Bump mockitoVersion from 3.12.4 to 4.11.0

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-14 09:52:15 -05:00
dependabot[bot] 5e9ead9efd
Bump com.github.kt3k.coveralls from 2.12.0 to 2.12.2 (#132)
Bumps com.github.kt3k.coveralls from 2.12.0 to 2.12.2.

---
updated-dependencies:
- dependency-name: com.github.kt3k.coveralls
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 17:14:08 -05:00
dependabot[bot] 7fc2406087
Bump org.bouncycastle:bcpkix-jdk15on from 1.66 to 1.70 (#131)
Bumps [org.bouncycastle:bcpkix-jdk15on](https://github.com/bcgit/bc-java) from 1.66 to 1.70.
- [Release notes](https://github.com/bcgit/bc-java/releases)
- [Changelog](https://github.com/bcgit/bc-java/blob/master/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcpkix-jdk15on
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-13 17:12:53 -05:00
dependabot[bot] 39b8c14650
Bump com.google.osdetector from 1.6.2 to 1.7.3 (#125)
Bumps com.google.osdetector from 1.6.2 to 1.7.3.

---
updated-dependencies:
- dependency-name: com.google.osdetector
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:46:25 -05:00
dependabot[bot] 6fe6a6eee2
Bump commons-cli:commons-cli from 1.4 to 1.5.0 (#124)
Bumps commons-cli:commons-cli from 1.4 to 1.5.0.

---
updated-dependencies:
- dependency-name: commons-cli:commons-cli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:46:07 -05:00
dependabot[bot] d14977c7ec
Bump org.projectlombok:lombok from 1.18.20 to 1.18.26 (#128)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.20 to 1.18.26.
- [Release notes](https://github.com/projectlombok/lombok/releases)
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.20...v1.18.26)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:45:32 -05:00
dependabot[bot] 72f352fc30
Bump org.apache.commons:commons-lang3 from 3.11 to 3.12.0 (#129)
Bumps org.apache.commons:commons-lang3 from 3.11 to 3.12.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:42:10 -05:00
dependabot[bot] 719554eab3
Bump actions/setup-java from 2 to 3 (#127)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:39:12 -05:00
dependabot[bot] ac7ec7da66
Bump actions/cache from 2 to 3 (#126)
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-13 16:35:47 -05:00
dependabot[bot] f949feb2d9
Bump actions/checkout from 2 to 3 (#123)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-13 16:33:53 -05:00
Max Lambrecht 479943d510
Create .github/dependabot.yml (#122)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-13 16:25:47 -05:00
Max Lambrecht 47edb81ae3
Prepare release 0.8.3 (#121)
Bump library version to 0.8.3
Add CHANGELOG.md

Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-12 15:51:58 -05:00
Max Lambrecht e5600c3f4d
Update gradle version to 8.0.2 (#104)
Update gradle version to 8.0.2

Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-12 13:16:22 -05:00
Guilherme Carvalho 0d0aae967f
Add SVID hints on workload api client (#114)
Add SVID hints on workload api client

Signed-off-by: Guilherme Carvalho <guilhermocc@proton.me>
2023-04-12 13:10:44 -05:00
Max Lambrecht c3dc4950e5
Fix CachedJwtSource tests (#120)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-10 15:02:27 -05:00
Max Lambrecht 50208a709a
Update grpc dependency (#117)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-05 16:40:47 -05:00
Max Lambrecht 11d0180134
Update netty dependency (#118)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-05 16:40:08 -05:00
Max Lambrecht 8c2d990e3f
Update nimbus-jose-jwt dependency (#119)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-05 16:33:26 -05:00
Max Lambrecht 3ca77c1de2
Add `CachedJwtSource` (#116)
Add CachedJwtSource

Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-04-04 13:49:32 -05:00
Max Lambrecht 5e16f7a632
Bump library version to 0.8.2 (#113)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-03-07 14:13:32 -06:00
Max Lambrecht f9dc354ae4
Solve memory leak. (#112)
Reverting changes that introduced a memory leak. Addressing error when context was cancelled

Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-03-07 13:30:51 -06:00
Max Lambrecht 942bcc9eb4
Add default constructor to SpiffeSslSocketFactory (#109)
Add default constructor to SpiffeSslSocketFactory

Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-02-16 16:13:21 -03:00
Max Lambrecht d9b932f782
Bump library to 0.8.1. Update README (#107)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-01-11 16:26:50 -03:00
Max Lambrecht 34e9b6c5fe
Updating Nimbus dependency version to 9.28 (#103)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-01-11 15:36:14 -03:00
Max Lambrecht c1c9be0c2e
Update SPIRE version for integration tests to 1.5.3 (#105)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-01-11 15:26:40 -03:00
Max Lambrecht 48991a0105
Fix close method in WorkloadApiClient (#102)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-01-11 15:00:07 -03:00
Thomas Richner 0085ce08f9
Aarch64 support (#101)
Add aarch64 module for OSX

Signed-off-by: Thomas Richner <thomas.richner@oviva.com>
Co-authored-by: Max Lambrecht <max.lambrecht@hpe.com>
2023-01-11 14:37:29 -03:00
Max Lambrecht a878d7d7f4
Update grpc-java dependency to 1.49.0 (#97)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2022-08-24 13:38:55 -07:00
Max Lambrecht 3ec46b414d
Update Nimbus JOSE-JWT dependency to 1.24.2 (#98)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2022-08-24 13:33:14 -07:00
Max Lambrecht 6eb2ed5c0f
Update version of SPIRE used in integration tests (#99)
Signed-off-by: Max Lambrecht <max.lambrecht@hpe.com>
2022-08-24 12:59:57 -07:00
Max Lambrecht 5840d8ff4e
Update README release 0.8.0 (#95) 2022-05-06 17:22:25 -03:00
Max Lambrecht 8120649796
Bump library version for release 0.8.0 (#94)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-05-06 16:30:07 -03:00
Max Lambrecht 4948b2bf2e
Update Gradle version to 7.4.2 (#93)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-04-29 14:18:03 -07:00
Max Lambrecht c0a4ddc089
Update Nimbus Jose-Jwt dependency to 9.22 (#92)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-04-29 14:08:47 -07:00
Max Lambrecht df4e0b3f71
Update grpc dependency to 1.46.0 (#91)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-04-29 14:06:02 -07:00
M1a0 6cdc17eb9c
Add FetchJWTSVIDs function for workloadapi and jwtSource (#90)
Signed-off-by: Yuhan Li <liyuhan.loveyana@bytedance.com>
2022-04-27 18:21:24 -03:00
Max Lambrecht 5a9fa55fe6
Update Nimbus JWT-JOSE dependency. (#89)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-02-07 17:47:36 -03:00
Max Lambrecht 0de5d0685b
Update grpc dependency. (#88)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-02-07 17:47:19 -03:00
Max Lambrecht f4e9f582fd
Update spire version in integration tests. (#87)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-02-07 17:47:02 -03:00
Max Lambrecht 66213fa047
Update gradle version. (#86)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2022-02-07 17:46:39 -03:00
Max Lambrecht cac2f603c4
Update artifact versions in READMEs (#85)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-09-08 15:23:00 -03:00
Max Lambrecht 77148473c4
Strict SPIFFE ID parsing (#74)
Strict SPIFFE ID parsing.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-09-08 14:03:24 -03:00
Max Lambrecht 5289d4bb98
Update integration tests SPIRE version (#84)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-09-07 11:48:49 -07:00
Max Lambrecht 96fa2db3de
Update Nimbus JWT-JOSE dependency (#82)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-09-07 11:48:14 -07:00
Max Lambrecht 87420066c7
Update grpc-java dependency (#81)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-09-07 11:10:49 -07:00
Ian Haken 744f5276b4
Add support to the SpiffeTrustManager for a SpiffeIdVerifier callback. (#77)
Add support to the SpiffeTrustManager for a SpiffeIdVerifier callback.

Signed-off-by: Ian Haken <ihaken@netflix.com>
2021-08-14 08:10:06 -03:00
Max Lambrecht 7edf338ac1
Make CI script use SPIRE 1.0.1 (#78)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-08-11 20:03:05 -07:00
Max Lambrecht 7657acfafd
Add Integration tests for Workload API client. (#75)
* Add integration tests and setup script.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
2021-07-27 10:08:32 -07:00
Ryan Turner 7e08d92859
Merge pull request #73 from maxlambrecht/add-github-actions
Configure Github Actions
2021-06-23 18:41:45 -07:00
Max Lambrecht 6a08cc3a60 Resolve nits.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-23 09:29:54 -03:00
Max Lambrecht 906995fd81 Refactor Github Actions config.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 13:37:41 -03:00
Max Lambrecht 1161be29a4 Remove unnecessary build.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 13:29:04 -03:00
Max Lambrecht 028302358d Remove CI name.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 13:14:15 -03:00
Max Lambrecht f01ee2cee7 Set coveralls CI name.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 12:52:23 -03:00
Max Lambrecht c40da6a305 Set coveralls CI name.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 12:47:33 -03:00
Max Lambrecht 30d751a579 Update secret name.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 12:37:24 -03:00
Max Lambrecht ffcb63309b Add build to coverage step.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 12:25:08 -03:00
Max Lambrecht c9c6aeae5a Add env variable to run coverage plugin.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 12:12:51 -03:00
Max Lambrecht 4109b2bdd4 Update coveralls gradle plugin.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 11:55:03 -03:00
Max Lambrecht 98153dd988 Fix Github Action config.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 11:42:18 -03:00
Max Lambrecht 7e298d72b1 Add OS/JDK matrix to Github Actions config.
Update build badge link.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 11:40:04 -03:00
Max Lambrecht c010b90c00 Add OS/JDK matrix to Github Actions config.
Update build badge link.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-22 11:36:18 -03:00
Max Lambrecht 817b1102a5 Add Github Actions.
Upgrade gradle to 7.1.
Update Lombok dependency.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-06-19 15:55:27 -03:00
Ryan Turner 7a994861ef
Merge pull request #72 from maxlambrecht/fix/update-proto
Update workload.proto with the lastest version from the SPIFFE repo.
2021-05-27 19:09:52 -07:00
Max Lambrecht 377225ac7b Update workload.proto with the lastest version from the SPIFFE repo.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-05-27 15:55:07 -03:00
Ryan Turner 060bec6f33
Merge pull request #70 from maxlambrecht/nimbus-dependency-update
Update JWT-JOSE dependency
2021-04-12 12:41:53 -07:00
Ryan Turner 1f53be1c6c
Merge branch 'master' into nimbus-dependency-update 2021-04-12 12:40:45 -07:00
Ryan Turner 16d02d65ba
Merge pull request #69 from maxlambrecht/netty-dependency-update
Update netty-transport-native-kqueue dependecy to 4.1.63
2021-04-12 12:40:01 -07:00
Ryan Turner 86f9546178
Merge branch 'master' into netty-dependency-update 2021-04-12 12:39:56 -07:00
Ryan Turner 2950899913
Merge pull request #68 from maxlambrecht/grpc-dependency-update
Update grpc-java dependency to 1.37.0
2021-04-12 12:39:24 -07:00
Max Lambrecht aec49499c5 Update JWT-JOSE dependency to 9.8.1
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-04-12 15:54:12 -03:00
Max Lambrecht 467b1873cd Update netty-transport-native-kqueue dependecy to 4.1.63
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-04-12 15:48:00 -03:00
Max Lambrecht b8f936e97a Update grpc-java dependency to 1.37.0
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-04-12 14:57:41 -03:00
Max Lambrecht 8c2cbbc3d5
Update library version to 0.7.0 (#66)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-03-15 17:22:11 -03:00
Ryan Turner 1177178a1e
Merge pull request #63 from maxlambrecht/fetch-x509-bundles
Implement FetchX509Bundles method on WorkloadAPI client
2021-03-15 12:49:32 -07:00
Max Lambrecht ede1878fd5 Addressing PR comments
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-03-15 09:59:15 -03:00
Max Lambrecht baa447b058 Add FetchX509Bundles method to Workload API client
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-03-08 18:44:21 -03:00
Max Lambrecht 9a6e805756
Update JWT-JOSE Nimbus dependency to 9.7 (#65)
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>

Co-authored-by: Ryan Turner <rturner3@users.noreply.github.com>
2021-03-08 18:41:57 -03:00
Ryan Turner 600c098d10
Merge pull request #64 from maxlambrecht/grpc-dependency-update
Update grpc-java dependency to 1.36.0
2021-03-08 10:04:07 -08:00
Max Lambrecht 41153174f9 Update grpc-java dependency to 1.36.0
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-03-08 13:25:46 -03:00
Max Lambrecht 0ee9ae28fa
Validate JWT 'typ' header. (#62)
* Validate JWT 'typ' header.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-02-08 16:05:36 -03:00
Ryan Turner e33417b10b
Merge pull request #60 from maxlambrecht/validate-jwt-alg
Validate 'alg' header when parsing JWT-SVIDs from tokens
2021-02-05 10:11:02 -08:00
Max Lambrecht de82ebf916 Minor exception messages improvements.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-02-05 10:37:44 -03:00
Max Lambrecht f8970af7f4 Validate 'alg' header when parsing JWT-SVIDs from tokens.
Some minor refactors and improvements.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-02-04 16:30:41 -03:00
Ryan Turner c285da249e
Merge pull request #57 from maxlambrecht/master
Upgrade grpc dependency to 1.35.0
2021-02-03 10:12:03 -08:00
Max Lambrecht 1b70bbf1d5 Upgrade grpc dependency to 1.35.0
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-02-03 11:47:08 -03:00
Ryan Turner 6551f6d131
Merge pull request #56 from maxlambrecht/master
Remove private/public key match verification.
2021-02-02 17:45:54 -08:00
Max Lambrecht 3049810a35 Remove private/public key match verification.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-02-02 21:06:55 -03:00
Max Lambrecht 45e3fc86a6
Merge pull request #54 from rturner3/java-spiffe-provider-doc-fix
Remove separator argument from toSetOfSpiffeIds() examples
2021-01-13 09:18:53 -03:00
Ryan Turner f7ffdea3b1 Remove separator argument from toSetOfSpiffeIds() examples
The ',' separator is not accepted as valid by the library. There is now
an override method which uses a default separator, so prefer that for
the examples, where the separator character is not particularly
relevant.

Signed-off-by: Ryan Turner <turner@uber.com>
2021-01-12 20:31:26 -08:00
Ryan Turner df9dd31685
Merge pull request #53 from maxlambrecht/add-missing-spiffeid-validations
Make SPIFFE ID and TrustDomain compliant with the specs
2021-01-06 12:09:46 -08:00
Max Lambrecht bd6ca04270 Addressing PR comments.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-01-06 10:02:46 -03:00
Ryan Turner 0053f22ee4
Merge pull request #51 from maxlambrecht/gradle-upgrade
Gradle upgrade to 6.7.1
2021-01-05 10:54:48 -08:00
Max Lambrecht 9757e62ad3 Add missing SPIFFE ID and Trust Domain maximum length validations.
Fix parse SPIFFE ID to preserve path case.
Refactoring tests.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2021-01-05 12:46:41 -03:00
Max Lambrecht e37b9b50e9 Upgrade gradle wrapper to 6.7.1
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-11-25 09:26:27 -03:00
Max Lambrecht accb81c8d6
Merge pull request #49 from maxlambrecht/master
Use google osdetector plugin instead of internal gradle class.
2020-11-24 15:11:10 -03:00
Max Lambrecht e7425fbd4e Revert gradle wrapper update.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-11-24 15:05:12 -03:00
Max Lambrecht 48e72f0412
Merge pull request #50 from rturner3/fix-readme
Fix typo in README.md
2020-11-24 15:00:27 -03:00
Ryan Turner a1757c5557 Fix typo in README.md
Signed-off-by: Ryan Turner <turner@uber.com>
2020-11-24 09:41:12 -08:00
Max Lambrecht aff23a7d79 Use google osdetector plugin instead of internal gradle class.
Upgrade gradle wrapper to 6.7.1

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-11-24 10:57:37 -03:00
Max Lambrecht 20d0efead2
Merge pull request #47 from srwaggon/patch-2
Update README.md
2020-11-14 10:16:21 -03:00
Max Lambrecht c04dd2163f
Merge pull request #46 from srwaggon/patch-1
Update README.md
2020-11-14 10:16:03 -03:00
Samuel Waggoner 1ff550ac43
Update README.md
Update example to match current code contract
2020-11-13 16:05:39 -08:00
Samuel Waggoner c3d118da45
Update README.md
Amend parent class in example
2020-11-13 16:01:53 -08:00
Max Lambrecht b67a2f1bd8
Merge pull request #45 from maxlambrecht/master
Add maintainer to CODEOWNERS file
2020-11-10 08:37:15 -03:00
Max Lambrecht 437e0b5469 Add maintainer to CODEOWNERS file
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-11-09 17:42:15 -03:00
Max Lambrecht aba2c2d272
Merge pull request #44 from maxlambrecht/bump-0.6.3
Bump to 0.6.3
2020-10-27 18:24:19 -03:00
Max Lambrecht 2f295c86ab
Merge pull request #43 from maxlambrecht/docs-improvements
Minor improvements in READMEs and javadocs.
2020-10-27 18:23:07 -03:00
Max Lambrecht ebb933557c
Merge pull request #42 from maxlambrecht/update-lombok-dependency
Updating lombok and test libraries dependencies.
2020-10-27 18:22:45 -03:00
Max Lambrecht 00c3407c33 Merge branch 'update-lombok-dependency' of https://github.com/maxlambrecht/java-spiffe into update-lombok-dependency 2020-10-27 18:21:45 -03:00
Max Lambrecht 2d3b57ad28 Updating lombok and test libraries dependencies.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 18:20:00 -03:00
Max Lambrecht c08462accf Merge remote-tracking branch 'upstream/master' into master 2020-10-27 18:17:29 -03:00
Max Lambrecht 815fb40cf4
Merge pull request #41 from maxlambrecht/nimbus-dependency-update
Updating nimbus jwt-jose dependency.
2020-10-27 18:15:43 -03:00
Max Lambrecht 4e53c3ec71
Merge pull request #40 from maxlambrecht/grpc-dependency-update
Updating gRPC and protobuf dependencies.
2020-10-27 18:15:08 -03:00
Max Lambrecht b9035b4dac Bump to 0.6.3
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 10:57:39 -03:00
Max Lambrecht dbd44a4db4 Minor improvements in READMEs and javadocs.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 10:52:22 -03:00
Max Lambrecht ddff049b5d Updating lombok and test libraries dependencies.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 10:32:38 -03:00
Max Lambrecht 56e22854f3 Updating nimbus jwt dependency.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 10:21:53 -03:00
Max Lambrecht 3cfa6a8ccd Updating gRPC and protobuf dependencies.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-27 09:34:08 -03:00
Max Lambrecht b94d8da155
Merge pull request #38 from maxlambrecht/master
Adding a note about java-spiffe-helper artifact
2020-10-02 11:30:56 -03:00
Max Lambrecht 057a048397 Add note to README to clarify that java-spiffe-helper artifact is published to Github releases.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-10-02 11:17:15 -03:00
Max Lambrecht 285545f704
Merge pull request #36 from maxlambrecht/master
Configuration, validation and logging improvements
2020-08-25 14:53:44 -03:00
Max Lambrecht c22affce9d Merge remote-tracking branch 'upstream/master' 2020-08-25 13:13:58 -03:00
Max Lambrecht 5a8d9d9056 Bump version to 0.6.2
Improve how the SpiffeTrustManager is configured to either validate SPIFFE IDs or acceptAny.
Validate the SslContextOptions.
Add visibility to some validation errors by logging warnings.
Improve log of the X509Source update.
Improve Spiffe Provider README.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-08-25 13:08:52 -03:00
Max Lambrecht 6283205310
Merge pull request #35 from maxlambrecht/master
Fix jwt-svid audience validation. Update dependencies.
2020-08-20 14:10:27 -03:00
Max Lambrecht d64db92d1f Bump version to 0.6.1
Upgrade gprc-java dependency to 1.31.1
Upgrade other dependencies.
Fix jwt-svid audience validation.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-08-20 13:40:47 -03:00
David Langhorst da11acbff4
Merge pull request #34 from maxlambrecht/master
Updating README and gradle tasks for publishing artifacts to maven central
2020-08-06 07:31:59 -07:00
Max Lambrecht 09149487a0 Tasks for signing and publishing artifacts to maven repository.
Add to README instruction to add the maven and gradle dependencies.
Minor additions to gradle build configs.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-08-04 14:39:35 -03:00
Max Lambrecht 1f853a2649 Merge branch 'v2-api' 2020-07-17 14:39:26 -03:00
Max Lambrecht 5ac8db7167 Merge branch 'master' into v2-api 2020-07-17 14:36:53 -03:00
Max Lambrecht b83a0e472c
Merge pull request #22 from maxlambrecht/v2-api
Define and implement v2 interface.
2020-07-17 14:12:17 -03:00
Max Lambrecht 43baf252e8 Add CODEOWNERS
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-17 13:02:02 -03:00
Max Lambrecht 2fcaf752d7 Introduce JwtSource interface and refactor JWT Source implementation.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-16 16:28:54 -03:00
Max Lambrecht e9df15e44b Refactoring to improve testability.
Add X509Source interface.
Add tests to cover provider module.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-16 15:08:09 -03:00
Max Lambrecht e81a936a96 Refactors and tests in java-spiffe-helper.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-15 14:41:28 -03:00
Max Lambrecht 0c542c198c Refactors and tests in java-spiffe-helper.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-14 15:50:53 -03:00
Max Lambrecht 3549c666a0 Improving test coverage of core module. Some refactors to improve code quality and testability.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-14 12:22:46 -03:00
Max Lambrecht f47e48f684 Minor amendments.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-09 09:12:24 -03:00
Max Lambrecht f663e75a27 Amendments in javadocs.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-08 15:33:50 -03:00
Max Lambrecht 8efdfe59fd Removing jdk ea as is still not supported by gradle.
Removing jdk 14 as is a short term version.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-07 14:15:10 -03:00
Max Lambrecht 3ea04c5c8e Replacing oracljdks by the openjdks version.
Removing short term versions.
Adding latest 15 and ea versions.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-07 14:03:51 -03:00
Max Lambrecht d310d7bbee Adding fetchJwtSvid method not requiring subject as parameter.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-06 16:21:06 -03:00
Max Lambrecht db4e78616e Minor improve to exception message.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-06 10:47:27 -03:00
Max Lambrecht 390df837da Remove jdk 8 in travis build for OSX.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-04 10:07:24 -03:00
Max Lambrecht e994ede993 Add MacOS support.
Amendments in javadocs.
Other minor changes.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-04 09:57:49 -03:00
Max Lambrecht fee4cc1b91 Add reference to JWT third library.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-02 15:42:47 -03:00
Max Lambrecht 35e85cee99 Minor refactors and documentation clarifying the watch methods in WorkloadApiClient.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-02 11:39:02 -03:00
Max Lambrecht b5f36cc932 Update gradle wrapper version to 6.5.1
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-01 15:01:34 -03:00
Max Lambrecht 0d0f25454c Revert "Replace oracle jdks by open jdks in travis config."
This reverts commit 57705374

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-01 14:44:36 -03:00
Max Lambrecht 57705374ee Replace oracle jdks by open jdks in travis config.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-01 13:30:46 -03:00
Max Lambrecht 58b36687b9 Refactor: extract WorkloadApiClient interface. Improving code and testing.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-01 13:29:41 -03:00
Max Lambrecht 9e592c1d36 Adding validation in ExponentialBackoffPolicy to prevent overflows
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-07-01 09:51:21 -03:00
Max Lambrecht 7b61cb8c47 Addressing PR comments:
- refactors to replace strings by enums
- improve code readability
- add and clarify documentation
- improve and fix logic in spiffe id parsing
- rename classes

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-30 14:19:07 -03:00
Max Lambrecht 167efbac89 Fixing typos
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-29 13:41:08 -03:00
Max Lambrecht cabcb56134 Add volatile to closed flags
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-29 12:06:33 -03:00
Max Lambrecht 9b428d316d Addressing code style issues in module java-spiffe-provider.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-26 16:04:57 -03:00
Max Lambrecht 0273e7be10 Addressing code style issues in module java-spiffe-helper.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-26 15:49:57 -03:00
Max Lambrecht 14fbae8fa2 Refactoring WorkloadApiClient to reduce complexity.
Addressing code style issues.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-26 15:34:25 -03:00
Max Lambrecht 7268c54a28 Fixing checkstyle issues.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-26 14:55:54 -03:00
Max Lambrecht 3e81bee7ff Fixing checkstyle issues.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-26 13:23:53 -03:00
Max Lambrecht 86c724e587 Fixing links in README.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-24 15:40:52 -03:00
Max Lambrecht 81584a90fb Refactoring and completing javadocs.
Rename property to improve clarity.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-24 15:14:16 -03:00
Max Lambrecht 7d11db4944 Addressing PR comments:
- refactor methods for creating JwtSource and X509Source instances
- completing and clarifying javadocs

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-24 14:09:19 -03:00
Max Lambrecht 0005bd5a1c Addressing PR comments:
- rename enum to improve clarity
- add missing validations to socket address parsing
- add test scenarios for address parsing
- improve Address javadoc to use the language of the SPIFFE spec
- some minor refactors

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-24 11:43:53 -03:00
Max Lambrecht 96d660ad3a Refactor string literals by enum.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 19:12:29 -03:00
Max Lambrecht 48aa4e6308 Removing SpiffeId and TrustDomain limit validations.
Improving javadocs.
Removing unused code.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 15:00:59 -03:00
Max Lambrecht c5f85756fc Making JWT and X509 SVID entities unmodifiable.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 12:57:26 -03:00
Max Lambrecht cbca3a1ec2 Refactor extracting methods to improve readability.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 12:46:11 -03:00
Max Lambrecht 5221f838eb Amendments in javadocs and README.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 11:28:51 -03:00
Max Lambrecht ca5511eb91 Addressing PR comments:
- refactor acceptedSpiffeIds from List to Set
- refactor tests
- renaming methods to improve clarity
- amendments in javadocs

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 11:26:00 -03:00
Max Lambrecht dbfb09f0f8 Refactor private key algorithm parameter. Create enum.
Other minor changes.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-23 09:25:59 -03:00
Max Lambrecht 4e1d0fb8c8 Fix trust domain host validation.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-22 16:28:52 -03:00
Max Lambrecht 7d3adfe7cf Rename enum and add comment.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-22 16:18:59 -03:00
Max Lambrecht c92c90e7ea Addressing PR comments:
- replace private key format and algorithm guessing by parameters
- refactor a few ifs to switch case
- add spiffeid and trust domain validations and tests
- remove all imports wildcards
- other minor changes to improve quality

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-22 16:08:17 -03:00
Max Lambrecht 538be3fa09 Minor typo fix.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 17:34:15 -03:00
Max Lambrecht fbbf17d0a3 Minor typo and grammar fixes.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 15:14:30 -03:00
Max Lambrecht 57076ef4f5 java-spiffe-provider: Code style improvements.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 14:32:32 -03:00
Max Lambrecht c909062618 java-spiffe-helper: Code style improvements.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 13:53:57 -03:00
Max Lambrecht 48564f1193 java-spiffe-core: Code style improvements.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 11:58:21 -03:00
Max Lambrecht a546070435 Fix jar manifest.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-19 10:34:19 -03:00
Max Lambrecht f967aa81a6 Fixing javadoc related warnings.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-18 11:29:39 -03:00
Max Lambrecht db57253657 Add javadoc and source jars generation. Fixing errors and warnings.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-18 11:03:06 -03:00
Max Lambrecht 2153452545 Rename root package to 'io.spiffe'.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-18 10:18:08 -03:00
Max Lambrecht edec6b3806 Filter out internal package for coveralls
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 15:14:07 -03:00
Max Lambrecht a51253c182 Coveralls sourceDir config added
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 13:45:36 -03:00
Max Lambrecht c19eca7217 Update coveralls plugin version to latest
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 11:30:20 -03:00
Max Lambrecht 14626e8831 Set jacocoReportPath for coveralls plugin
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 11:27:51 -03:00
Max Lambrecht 8641f92dd1 Fix coverage output file for coveralls
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 11:22:56 -03:00
Max Lambrecht e41e80ce8e Add coveralls integration
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-17 11:05:17 -03:00
Max Lambrecht affac4e7bd Fix test dependency import.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-16 10:20:14 -03:00
Max Lambrecht e124009250 Minor amendments in javadoc comments.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-15 13:47:43 -03:00
Max Lambrecht f132b04a17 Amendments in javadoc in java-spiffe-helper.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-15 13:36:28 -03:00
Max Lambrecht 0e7413ad09 Adding and improving javadocs in the java-spiffe-provider module.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-15 13:28:42 -03:00
Max Lambrecht d00c6072bf Adding and improving javadocs. Removing unused code.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-15 11:18:16 -03:00
Max Lambrecht f3b2a411fc Addressing PR comments: improving names, javadoc amendments, code refactors.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-15 10:18:48 -03:00
Max Lambrecht 5c1d2762e7 Add javax.annotation-api dependency as workaround for Java 11+ versions.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-13 14:48:33 -03:00
Max Lambrecht 53244bb499 Addressing PR comments: replacing grpc netty dependencies by the recommended grpc-netty-shaded.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-13 13:15:26 -03:00
Max Lambrecht 2f7c83452a Fix error in TrustManager validation.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-13 12:52:21 -03:00
Max Lambrecht 874c7a5a88 Adding configuration for accepting any SPIFFE ID.
Amend X509 wordings.
Adding documentation in README.
Improvements.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-12 15:59:08 -03:00
Max Lambrecht 46d6fc0ade Fix code issues.
Improve README.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-12 08:06:07 -03:00
Max Lambrecht df234b5255 minor readme amendments
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 17:14:10 -03:00
Max Lambrecht 00eb86949f Addressing PR comments.
Adding documentation.
Amendments in READMEs and javadoc comments.
Some refactors to improve code and clarity.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 17:09:55 -03:00
Max Lambrecht f5c3b18d5a
Merge pull request #31 from maxlambrecht/travis_integration
Run build/test against all supported Java versions
2020-06-11 12:42:34 -03:00
Max Lambrecht 6d42e48861 Add all supported java versions to travis build.
Extract common string versions to constants in build.gradle.
Update some dependency version.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 11:35:34 -03:00
Max Lambrecht 20f84bbd82 Add jdk condition to deploy config.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 10:58:14 -03:00
Max Lambrecht f7f36d9c6e Replace deprecated oraclejdk10 by openjdk10
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 10:38:17 -03:00
Max Lambrecht 8f9c569010 Execute the build/tests against all Java SE versions supported.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-11 10:32:53 -03:00
Max Lambrecht b571536029 Run gradle task copyJars after assemble.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-10 14:47:11 -03:00
Max Lambrecht af2ee3d27b Move modules versions to a single parent version to simplify versioning.
Add gradle task to copy generated jars artifacts to a common folder.
Add travis config to deploy the jars.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-10 14:43:13 -03:00
Max Lambrecht 6164efa72a
Merge pull request #30 from maxlambrecht/travis_integration
Add github releases to travis config
2020-06-10 11:54:23 -03:00
Max Lambrecht 2905f008cc minor: add blank line at the end
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-10 11:51:07 -03:00
Max Lambrecht 77792170ac Add github releases to travis config
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-10 11:05:39 -03:00
Max Lambrecht 91eb49bd8e Update grpc, netty and jwt-jose dependencies
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-09 14:01:07 -03:00
Max Lambrecht cccc243829 Set core dependency as api
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-09 13:51:45 -03:00
Max 312d275264
Merge pull request #27 from maxlambrecht/travis_integration
Add travis build config
2020-06-09 10:49:48 -03:00
Max Lambrecht ee8ba44fd4 Add travis build config
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-09 10:33:39 -03:00
Max Lambrecht 6ffe5056ed Adding travis integration.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-09 10:22:43 -03:00
Max Lambrecht bee7627afa Minor: fixing gradle warnings
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-09 09:04:55 -03:00
Max Lambrecht d005100a23 Update gradle-wrapper version to latest: 6.5
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-08 15:08:27 -03:00
Max Lambrecht ea0d363c68 Refactor BundleSource interfaces to have a single parameterized interface for both X509 and JWT bundles.
Minor code improvements.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-08 14:56:51 -03:00
Max Lambrecht d68c17caaa Moving class to avoid circular references.
Rename type parameters.
Improve readability.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-08 14:37:02 -03:00
Max Lambrecht 28ad4c0c0c Improve Private and Public Keys verification.
Add tests for keys verification.
Resolve TODO in interface documentation.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-08 14:01:37 -03:00
Max Lambrecht cf761c5bdf Java Spiffe Helper implementation
Refactors
Tests
README improvements

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-06-04 10:19:19 -03:00
Max Lambrecht 5b27a2fc86 Implementing
- JWT functionality in Workload API client.
- JWT Source.
- Fake Workload API.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-28 11:16:01 -03:00
Max Lambrecht cd64eb7966 Adding test to cover EC private key generation.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-22 09:53:30 -03:00
Max Lambrecht ef2cdafab9 Adding utility methods for generating x509 certificates for testing purposes.
Adding jacoco test coverage report plugin.
Adding and improving tests for X509SvidValidator and CertificateUtils.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-22 09:31:32 -03:00
Max Lambrecht ef4dbf86c5 Adding tests for BackoffPolicy and RetryHandler.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-20 15:14:19 -03:00
Max Lambrecht 0853442026 Adding tests.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-20 14:09:30 -03:00
Max Lambrecht 87c8aadeba Refactoring Algorithm and Family as enums.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-20 13:51:10 -03:00
Max Lambrecht 21514c7a16 Renaming Algorithm class
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-20 13:23:39 -03:00
Max Lambrecht e160708d4e Refactoring JWT SVID and bundle.
Replace JWT library using nimbus for both JWT and JOSE bundles.
Refactoring tests
Adding tests for improving coverage.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-20 13:17:19 -03:00
Max Lambrecht 8c5384ee3b Implementing JWT bundle and bundle set.
Refactors to X509 bundle and set.
Adding tests.
Adding library for processing JOSE JWK bundles.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-19 13:07:07 -03:00
Max 25cb640190
Merge pull request #25 from spiffe/dev
Merge dev into master
2020-05-18 14:39:15 -03:00
Max c7c7cf3b32
Merge pull request #24 from maxlambrecht/dev
Avoid netty warning by setting unknown channel option to null
2020-05-18 14:35:21 -03:00
Max Lambrecht a203cf450f Avoid warning Unknown channel option 'SO_KEEPALIVE' on Mac
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 13:58:33 -03:00
Max Lambrecht c1cdce9c79 Minor format fix
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 13:24:45 -03:00
Max Lambrecht 2e645a0bac Avoid netty warning by setting unknown channel option to null, for both linux and mac.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 13:20:53 -03:00
Max Lambrecht 06cb19ca08 Avoid netty warning by setting unknown channel option to null
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 13:14:48 -03:00
Max Lambrecht a58aa9925b Avoid warning Unknown channel option 'SO_KEEPALIVE'
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 12:51:02 -03:00
Max Lambrecht 11d00e191c Adding tests to improve coverage for X509Svid, X509SvidValidator, X509BundleSet, SpiffeId, JwtSvid.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 12:11:47 -03:00
Max Lambrecht fa50d55dd0 Change exception thrown by validation method and adding tests
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-18 09:00:54 -03:00
Max Lambrecht ec681cbf99 Add defensive copy to prevent vulnerability
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-17 09:54:43 -03:00
Max Lambrecht 3ef62e0812 Adding and refactoring tests
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-17 09:42:19 -03:00
Max Lambrecht 5abd112c58 X509 SVID: adding validations and tests
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-16 14:10:33 -03:00
Max Lambrecht 8d9bbc065a JWT SVID implementation
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-16 07:52:32 -03:00
Max Lambrecht 12329d924d Adding method to TrustDomain and test
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-13 11:08:25 -03:00
Max Lambrecht 588da2cd55 Adding documentation
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-08 09:44:53 -03:00
Max Lambrecht 6565ea1029 Minor change
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-08 08:02:08 -03:00
Max Lambrecht 19bf9730b7 Remove unnecessary method
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-08 07:58:38 -03:00
Max Lambrecht f8a176dc20 Change method name to improve clarity.
Fix build warning.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-08 07:36:03 -03:00
Max Lambrecht 9867c032cf Move examples to a folder in tests.
Add details to README.
Make constants public.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-07 09:03:42 -03:00
Max Lambrecht 4e35b003fc Update workload.proto.
Validate that the X.509 context has both the svid and the bundles in the X509 context watcher.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-06 15:56:57 -03:00
Max Lambrecht 8de0b766ab Adding extra method to create X509 source, improving some javadoc comments.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-06 15:36:12 -03:00
Max Lambrecht 29daad1c5b Adding timeout to X509Source new method.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-06 11:16:20 -03:00
Max Lambrecht 8027b39298 Minor change: making field final
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-04 08:59:48 -03:00
Max 89380590b8 Configure workload api client and netty channel with the ExecutorService and shutdown the underlying channel properly.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-04 08:56:58 -03:00
Max Lambrecht b0bac0c29a Setting the defaults for the BackoffPolicy in the builder constructor
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-01 08:44:46 -03:00
Max Lambrecht 30c0ddb5e2 Minor fix in method signature
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-01 06:52:42 -03:00
Max Lambrecht 0c4801cf49 Remove double synchronization that is no longer recommended, add synchronized to method instead
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-05-01 05:32:04 -03:00
Max Lambrecht c7212b9c76 Make constructor private in X509SourceManager
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-30 13:17:34 -03:00
Max 7d12743fb5 Adding backoff retry to watchX509Context.
Changing X509ContextException and X509ContextException to make them checked.
Address multiple PR comments.
Adding tests to Address and TrustDomain.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-30 09:33:46 -03:00
Max 8e64bb63a0 Improve methods to avoid unnecessary extra get operation
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-28 12:26:08 -03:00
Max Lambrecht 96b27c8277 Amending names and comments
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-27 06:47:28 -03:00
Max Lambrecht 571dd3c5d4 Minor change in logging
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-27 06:32:33 -03:00
Max Lambrecht c26f0339a6 Delete unused classes
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-27 06:18:03 -03:00
Max Lambrecht bf6053c18c Process federated bundles from X509Context update
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-26 16:31:04 -03:00
Max Lambrecht ecabc0f288 Minor fixes
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-26 15:16:11 -03:00
Max Lambrecht 44cda6e809 Refactor error handling: use Exceptions instead of Result wrapper type
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-26 13:58:21 -03:00
Max Lambrecht 8e06cb12d7 Refactor: changing name jwtKeys and x509Roots by jwtAuthorities and x509Authorities
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-23 11:42:18 -03:00
Max Lambrecht 928b075661 Moving and cleaning dependencies
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-23 07:39:55 -03:00
Max Lambrecht 91f06d83bc Adding more info in readme and completing an example
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-22 11:54:14 -03:00
Max Lambrecht 219a2e2e71 Refactoring SSL Context factory. Refactoring WorkloadAPIClient and X509Source
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-22 10:01:55 -03:00
Max Lambrecht 2cccc1c988 Refactoring Results in multiple methods. Refactoring the X509Source config.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-21 13:01:26 -03:00
Max Lambrecht 15978d8313 Add comments to provide more details about the X509Source
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-20 12:14:35 -03:00
Max Lambrecht 07169bf411 Add Exception to error result
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-20 11:49:45 -03:00
Max Lambrecht 5eb49938ba Make FetchJwtSvid return a Result
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-20 11:26:50 -03:00
Max Lambrecht 3e05e37b10 Changing return value from Optional to Result. Other refactors. Improving comments.
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-20 10:56:36 -03:00
Max Lambrecht 43f9d79e8c [WIP]-Define v2 interface, refactor in modules
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
2020-04-10 14:21:27 -03:00
250 changed files with 19333 additions and 1831 deletions

110
.dockerignore Normal file
View File

@ -0,0 +1,110 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# GitHub
.github
# Git
.git

View File

@ -0,0 +1,49 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: java-spiffe-helper
data:
java-spiffe-helper.properties: |
keyStorePath=/tmp/keystore.p12
keyStorePass=password
keyPass=password
trustStorePath=/tmp/truststore.p12
trustStorePass=password
keyStoreType=pkcs12
keyAlias=spiffe
spiffeSocketPath=unix:/run/spire/agent-sockets/spire-agent.sock
---
apiVersion: v1
kind: Pod
metadata:
name: java-spiffe-helper
labels:
app: java-spiffe-helper
spec:
containers:
- name: java-spiffe-helper
image: java-spiffe-helper:test
imagePullPolicy: IfNotPresent
readinessProbe:
initialDelaySeconds: 15
exec:
command:
- ls
- /tmp/truststore.p12
volumeMounts:
- name: properties
mountPath: /app/java-spiffe-helper.properties
subPath: java-spiffe-helper.properties
- name: spire-sockets
mountPath: /run/spire/agent-sockets
readOnly: true
restartPolicy: Never
volumes:
- name: properties
configMap:
name: java-spiffe-helper
- name: spire-sockets
hostPath:
path: /run/spire/agent-sockets
type: DirectoryOrCreate

View File

@ -0,0 +1,18 @@
spire-server:
ca_subject:
common_name: common_name
controllerManager:
identities:
clusterSPIFFEIDs:
default:
enabled: false
java-spiffe-helper:
spiffeIDTemplate: spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
podSelector:
matchLabels:
app: java-spiffe-helper
dnsNameTemplates:
- dnsNameTemplate

15
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

79
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Build
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
build-and-test-on-linux:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ 8, 11, 17, 21 ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'adopt'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle and generate the artifacts (also run the tests)
run: ./gradlew build
- name: Run integration tests
run: ./.github/workflows/scripts/integration-tests.sh
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
build-and-test-on-macos:
runs-on: macos-latest
strategy:
matrix:
java-version: [ 8, 11, 17, 21 ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'zulu'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle and generate the artifacts
run: ./gradlew build
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties

28
.github/workflows/coverage.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: coverage
on:
push:
branches: [ main ]
jobs:
create-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '16'
distribution: 'adopt'
- name: Generate and upload coverage report
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: ./gradlew jacocoTestReport coveralls
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties

View File

@ -0,0 +1,85 @@
name: Java SPIFFE Helper CI
on:
- pull_request
jobs:
test:
name: Test with SPIRE helm chart in version ${{ matrix.spire-chart-version.spire }}
runs-on: ubuntu-latest
strategy:
matrix:
spire-chart-version:
- spire: '0.17.x'
crds: '0.3.x'
env:
HELM_REPOSITORY: https://spiffe.github.io/helm-charts-hardened/
KEYSTORE_COMMON_NAME: keystore-${{ github.sha }}
TRUSTSTORE_COMMON_NAME: truststore-${{ github.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup buildx
uses: docker/setup-buildx-action@v3
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Build container
uses: docker/build-push-action@v6
with:
context: .
tags: java-spiffe-helper:test
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create local kubernetes cluster
uses: helm/kind-action@v1
with:
cluster_name: kind
- name: Load container image onto kubernetes node
run: kind load docker-image java-spiffe-helper:test --name kind
- name: Install SPIRE CRDs in version ${{ matrix.spire-chart-version.crds }}
run: |
helm upgrade --install -n spire-server spire-crds spire-crds \
--repo ${{ env.HELM_REPOSITORY }} \
--version ${{ matrix.spire-chart-version.crds }} \
--create-namespace
- name: Install SPIRE server in version ${{ matrix.spire-chart-version.spire }} and set to-be-verified values for common name
run: |
helm upgrade --install -n spire-server spire spire \
--repo ${{ env.HELM_REPOSITORY }} \
--version ${{ matrix.spire-chart-version.spire }} \
--values .github/ci-k8s-configs/spire-values.yaml \
--set spire-server.ca_subject.common_name="$TRUSTSTORE_COMMON_NAME" \
--set spire-server.controllerManager.identities.clusterSPIFFEIDs.java-spiffe-helper.dnsNameTemplates[0]="$KEYSTORE_COMMON_NAME"
- name: Deploy java-spiffe-helper pod to local cluster
run: kubectl apply -f .github/ci-k8s-configs/java-spiffe-helper.yaml
- name: Wait for java-spiffe-helper pod to become ready
run: kubectl wait pod/java-spiffe-helper --for condition=Ready --timeout=90s
- name: Output logs of java-spiffe-helper pod
if: ${{ failure() }}
run: kubectl logs pod/java-spiffe-helper
- name: Describe java-spiffe-helper pod
if: ${{ failure() }}
run: kubectl describe pod/java-spiffe-helper
- name: Copy keystore from java-spiffe-helper pod
run: kubectl cp java-spiffe-helper:/tmp/keystore.p12 keystore.p12
- name: Copy truststore from java-spiffe-helper pod
run: kubectl cp java-spiffe-helper:/tmp/truststore.p12 truststore.p12
- name: Verify keystore contains configured common name
run: keytool -v -list -keystore keystore.p12 -storepass password | grep "CN=${{ env.KEYSTORE_COMMON_NAME }}"
- name: Output keystore contents
if: ${{ failure() }}
run: keytool -v -list -keystore keystore.p12 -storepass password
- name: Verify truststore contains configured common name
run: keytool -v -list -keystore truststore.p12 -storepass password | grep "CN=${{ env.TRUSTSTORE_COMMON_NAME }}"
- name: Output truststore contents
if: ${{ failure() }}
run: keytool -v -list -keystore truststore.p12 -storepass password

59
.github/workflows/release_build.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: Release Build
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
env:
REGISTRY: ghcr.io
JAVA_VERSION: '17'
jobs:
publishToMaven:
runs-on: ubuntu-latest
env:
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_TOKEN: ${{ secrets.NEXUS_TOKEN }}
PGP_PRIVATE_KEY: ${{ secrets.PGP_PRIVATE_KEY }}
PGP_KEY_PASSPHRASE: ${{ secrets.PGP_KEY_PASSPHRASE }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'adopt'
- name: Publish to Nexus Maven Repository
run: ./gradlew publish
publishDockerImage:
needs: publishToMaven
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Determine Docker Tag
run: echo "DOCKER_TAG=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: Publish java-spiffe-helper Docker Image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY }}/${{ github.repository }}-helper:${{ env.DOCKER_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Start a SPIRE Server and Agent and run the integration tests
# Only works on Linux.
set -euf -o pipefail
export SPIFFE_ENDPOINT_SOCKET="unix:/tmp/spire-agent/public/api.sock"
spire_version="1.11.0"
spire_folder="spire-${spire_version}"
spire_server_log_file="/tmp/spire-server/server.log"
spire_agent_log_file="/tmp/spire-agent/agent.log"
function cleanup() {
killall -9 spire-agent || true
killall -9 spire-server || true
rm -f /tmp/spire-server/private/api.sock
rm -f /tmp/spire-agent/public/api.sock
rm -rf ${spire_folder}
}
# Some cleanup: kill spire processes that could have remained from previous run
trap cleanup EXIT
# Install and run a SPIRE server
curl -s -N -L https://github.com/spiffe/spire/releases/download/v${spire_version}/spire-${spire_version}-linux-amd64-musl.tar.gz | tar xz
pushd "${spire_folder}"
mkdir -p /tmp/spire-server
bin/spire-server run -config conf/server/server.conf > "${spire_server_log_file}" 2>&1 &
spire_server_started=0
for i in {1..10}
do
if bin/spire-server healthcheck >/dev/null 2>&1; then
spire_server_started=1
break
fi
sleep 1
done
if [ ${spire_server_started} -ne 1 ]; then
cat ${spire_server_log_file} >&2
echo 'SPIRE Server failed to start' >&2
exit 1
fi
# Generate token and run Spire Agent
agent_id="spiffe://example.org/myagent"
bin/spire-server token generate -spiffeID ${agent_id} > token
cut -d ' ' -f 2 token > token_stripped
mkdir -p /tmp/spire-agent
bin/spire-agent run -config conf/agent/agent.conf -joinToken "$(< token_stripped)" > "${spire_agent_log_file}" 2>&1 &
spire_agent_started=0
for i in {1..10}
do
if bin/spire-agent healthcheck >/dev/null 2>&1; then
spire_agent_started=1
break
fi
sleep 1
done
if [ ${spire_agent_started} -ne 1 ]; then
cat ${spire_agent_log_file} >&2
echo 'SPIRE Agent failed to start' >&2
exit 1
fi
# Register the workload through UID with the SPIFFE ID "spiffe://example.org/myservice"
bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/myservice -selector unix:uid:$(id -u)
sleep 10 # this value is derived from the default Agent sync interval
popd
# Run only the integration tests
./gradlew integrationTest

125
CHANGELOG.md Normal file
View File

@ -0,0 +1,125 @@
# Changelog
## [0.8.12] - 2025-06-05
### Dependency updates
- Bump grpcVersion to 1.73.0 (#327)
- Bump com.nimbusds:nimbus-jose-jwt to 10.3 (#323)
- Bump io.netty:netty-transport-native-kqueue to 4.2.1.Final (#321)
- Bump com.google.protobuf:protobuf-gradle-plugin to 0.9.5 (#314)
- Bump org.projectlombok:lombok to 1.18.38 (#313)
### Documentation
- Add documentation about sslNegotiation config in java-spiffe-provider (#325)
## [0.8.11] - 2024-11-18
### Fixed
- Fix CRLF newline removal in DER format logic for Windows compatibility (#284)
### Dependency updates
- Bump io.netty:netty-transport-native-kqueue from 4.1.114.Final to 4.1.115.Final (#281)
- Bump com.nimbusds:nimbus-jose-jwt from 9.45 to 9.47 (#285)
- Bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#286)
## [0.8.10] - 2024-11-06
### Dependency updates
- Bump jupiterVersion from 5.11.2 to 5.11.3 (#273)
- Bump grpcVersion from 1.68.0 to 1.68.1 (#276)
- Bump com.nimbusds:nimbus-jose-jwt from 9.41.2 to 9.45 (#278)
## [0.8.9] - 2024-10-09
### Dependency updates
- Bump `com.google.protobuf:protoc` to `3.25.5` (#271)
## [0.8.8] - 2024-10-08
### Dependency updates
- Bump `grpcVersion` from 1.66.0 to 1.68.0 (#262)
- Bump `io.netty:netty-transport-native-kqueue` from 4.1.113.Final to 4.1.114.Final (#265)
- Bump `com.nimbusds:nimbus-jose-jwt` from 9.41.1 to 9.41.2 in (#266)
### Changed
- Updated Gradle to version 8.10.2 (#269)
## [0.8.7] - 2024-09-20
### Dependency updates
- Bump `grpcVersion` from 1.62.2 to 1.66.0 (#248)
- Bump `io.netty:netty-transport-native-kqueue` from 4.1.107.Final to 4.1.113.Final (#260)
- Bump `commons-cli:commons-cli` from 1.6.0 to 1.9.0 (#258)
- Bump `com.nimbusds:nimbus-jose-jwt` from 9.37.3 to 9.41.1 (#259)
- Bump `org.apache.commons:commons-lang3` from 3.14.0 to 3.17.0 (#255)
- Bump `org.projectlombok:lombok` from 1.18.30 to 1.18.34 (#253)
- Bump `commons-validator:commons-validator` from 1.8.0 to 1.9.0 (#251)
- Bump `jupiterVersion` from 5.10.2 to 5.11.0 (#254)
## [0.8.6] - 2024-03-04
### Dependency updates
- Bump `com.google.protobuf:protoc` from 3.25.2 to 3.25.3 (#218)
- Bump `io.grpc:grpc-protobuf`, `io.grpc:grpc-stub`, `io.grpc:grpc-netty`, `io.grpc:grpc-netty-shaded`,
and `io.grpc:protoc-gen-grpc-java` from 1.61.1 to 1.62.2 (#222)
- Bump `io.netty:netty-transport-native-kqueue` from 4.1.106.Final to 4.1.107.Final (#205)
### CI/CD Improvements
Automated build and publish process via GitHub Actions.
## [0.8.5] - 2024-14-02
### Added
- Docker container and CI workflow for `java-spiffe-helper` (#187)
### Changed
- Updated Gradle to version 8.5 (#201)
- Various enhancements in `java-spiffe-helper` (#199)
### Fixed
- Addressed a Fat Jar Assembly issue. (#198)
### Dependency updates
- Bump `io.grpc:grpc-protobuf` and `io.grpc:grpc-stub` from 1.54.0 to 1.61.1 (#202)
- Bump `commons-validator:commons-validator` from 1.7. to 1.8.0 (#197)
- Bump `commons-cli:commons-cli` from 1.5.0 to 1.6.0 (#196)
- Bump `com.google.protobuf:protoc` from 3.21.12 to 3.25.2 (#193)
- Bump `io.netty:netty-transport-native-kqueue` from 4.1.91.Final to 4.1.106.Final (#192)
- Bump `org.apache.commons:commons-lang3` from 3.12.0 to 3.14.0 (#189)
- Bump `com.nimbusds:nimbus-jose-jwt` from 9.31 to 9.37.3 (#184)
- Bump `org.projectlombok:lombok` from 1.18.26 to 1.18.30 (#170)
- Bump `com.google.protobuf:protobuf-gradle-plugin` from 0.9.2 to 0.9.4 (#153)
## [0.8.4] - 2023-04-14
### Dependencies updates
- Bump `commons-cli:commons-cli` from 1.4 to 1.5.0 (#124)
- Bump `com.google.osdetector` from 1.6.2 to 1.7.3 (#125)
- Bump `org.apache.commons:commons-lang3` from 3.11 to 3.12.0 (#129)
- Bump `org.projectlombok:lombok` from 1.18.20 to 1.18.26 (#128)
## [0.8.3] - 2023-04-13
### Added
- A `JwtSource` implementation,`CachedJwtSource`, that caches the JWT SVIDs based on their subjects and audiences (#116)
- Support for the `hint` field in the SVIDs retrieved by Workload API client (#114)

13
CODEOWNERS Normal file
View File

@ -0,0 +1,13 @@
* @maxlambrecht @rturner3
##########################################
# Maintainers
##########################################
# Max Lambrecht
# Hewlett-Packard Enterprise
# @maxlambrecht
# Ryan Turner
# Uber Technologies, Inc.
# @rturner3

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /builder
COPY . /builder
RUN ./gradlew dependencies
RUN ./gradlew java-spiffe-helper:assemble -ParchiveClassifier=docker -Pversion=docker
FROM eclipse-temurin:17-jre AS runner
USER nobody
COPY conf/java-spiffe-helper.properties /app/java-spiffe-helper.properties
COPY --from=builder /builder/java-spiffe-helper/build/libs/java-spiffe-helper-docker-docker.jar /app/java-spiffe-helper.jar
ENTRYPOINT ["java", "-jar", "/app/java-spiffe-helper.jar"]
CMD ["--config", "/app/java-spiffe-helper.properties"]

253
README.md
View File

@ -1,209 +1,140 @@
# JAVA-SPIFFE library
# Java SPIFFE Library
[![Build Status](https://github.com/spiffe/java-spiffe/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/spiffe/java-spiffe/actions/workflows/build.yml?query=branch%3Amain)
[![Coverage Status](https://coveralls.io/repos/github/spiffe/java-spiffe/badge.svg)](https://coveralls.io/github/spiffe/java-spiffe?branch=main)
## Overview
The JAVA-SPIFFE library provides two components:
- a Client that implements the functionality to fetch SVIDs Bundles from a Workload API.
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X.509 and JWT SVIDs and Bundles,
and a Java Security Provider implementation to be plugged into the Java Security architecture. This is essentially
an X.509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
asynchronously from the Workload API. The KeyStore handles the Certificate chain and Private Key to prove identity
in a TLS connection, and the TrustStore handles the trusted bundles (supporting federated bundles) and performs
peer's certificate and SPIFFE ID verification.
- an SVID based KeyStore and TrustStore implementation that handles the certificates in memory
and receives the updates asynchronously from the Workload API. Using the terminology of the Java Security API,
this library provides a custom Security Provider that can be installed in the JVM.
This library contains three modules:
It supports Federation. The TrustStore validates the peer's SVID using a set of Trusted CAs that includes the
Federated TrustDomains CAs bundles. These Federated CAs bundles come from the Workload API in the X509SVIDResponse.
* [java-spiffe-core](java-spiffe-core/README.md): Core functionality to interact with the Workload API, and to process and validate
X.509 and JWT SVIDs and bundles.
Besides, this library provides a SocketFactory implementation to support TCP connections.
* [java-spiffe-provider](java-spiffe-provider/README.md): Java Provider implementation.
## SPIFFE Workload API Client Example
* [java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X.509 SVIDs and Bundles in Java Keystores in disk.
The `X509SVIDFetcher` provides the `registerListener` method that allows a consumer to register a listener
to get the X509-SVIDS whenever the Workload API has a new SVID to push.
**Supports Java 8+**
The gRPC channel is configured based on the Address (tcp or unix socket) and the OS detected.
Download
--------
### Build the JAR
The JARs can be downloaded from [Maven Central](https://search.maven.org/search?q=g:io.spiffe%20AND%20v:0.8.12).
To create a fat JAR file that includes all the dependencies:
The dependencies can be added to `pom.xml`
To import the `java-spiffe-provider` component:
```xml
<dependency>
<groupId>io.spiffe</groupId>
<artifactId>java-spiffe-provider</artifactId>
<version>0.8.12</version>
</dependency>
```
$ ./gradlew build
BUILD SUCCESSFUL in 2s
The `java-spiffe-provider` component imports the `java-spiffe-core` component.
To just import the `java-spiffe-core` component:
```xml
<dependency>
<groupId>io.spiffe</groupId>
<artifactId>java-spiffe-core</artifactId>
<version>0.8.12</version>
</dependency>
```
In folder `build/libs` there will be a file `spiffe-provider-<version>-all.jar`.
Using Gradle:
To create a slim JAR file:
```
$ ./gradle jar
BUILD SUCCESSFUL in 1s
Import `java-spiffe-provider`:
```gradle
implementation group: 'io.spiffe', name: 'java-spiffe-provider', version: '0.8.12'
```
In folder `build/libs` there will be a file `spiffe-provider-<version>.jar`.
### Use
The library provides a `SpiffeIdManager` that abstracts low level details related to the interaction with the WorkloadAPI and exposes
getter methods to obtain the SVID, Bundle and Key:
```
SpiffeIdManager spiffeIdManager = SpiffeIdManager.getInstance();
PrivateKey privateKey = spiffeIdManager.getPrivateKey();
X509Certificate svid = spiffeIdManager.getCertificate();
Set<X509Certificate> bundle = spiffeIdManager.TrustedCerts();
Import `java-spiffe-core`:
```gradle
implementation group: 'io.spiffe', name: 'java-spiffe-core', version: '0.8.12'
```
The `SpiffeIdManager` gets the certificate updates automatically from the WorkloadAPI.
### MacOS Support
It uses a `X509SVIDFetcher` that handles the interaction with the WorkloadAPI.
#### x86 Architecture
The path to the Socket where the Workload API is listening needs to configured either by setting the system property `-Dspiffe.endpoint.socket` or
or an the environment variable `SPIFFE_ENDPOINT_SOCKET`.
In case run on a osx-x86 architecture, add to your `pom.xml`:
```xml
Another way to use the library is by directly instantiating the `X509SVIDFetcher` and registering a callback (aka Consumer)
that will be invoked whenever there is an update pushed by the Workload API:
```
Fetcher<X509SVIDResponse> svidFetcher = new X509SVIDFetcher("/tmp/agent.sock");
Consumer<X509SVIDResponse> xvidConsumer = x509SVIDResponse -> {
x509SVIDResponse.getSvidsList().forEach(svid -> {
System.out.println("Spiffe ID fetched: " + svid.getSpiffeId());
System.out.println("Federated with: " + svid.getFederatesWithList());
});
System.out.println(x509SVIDResponse.getFederatedBundlesMap());
};
//Registering the callback to receive the SVIDs from the Workload API
svidFetcher.registerListener(xvidConsumer);
<dependency>
<groupId>io.spiffe</groupId>
<artifactId>grpc-netty-macos</artifactId>
<version>0.8.12</version>
<scope>runtime</scope>
</dependency>
```
In this case the path to the Socket is passed through a parameter in the constructor. If the parameter is not provided, it will
use the system property, if it is defined, or the environment variable. If neither is defined, it will throw an Exception.
The `X509SVIDFetcher` can be configured with a custom `RetryPolicy`.
By default it uses a `RetryPolicy` with the following parameters:
```
initialDelay = 1;
maxDelay = 300;
timeUnit = SECONDS;
expBackoffBase = 2
maxRetries = UNLIMITED_RETRIES;
Using Gradle:
```gradle
runtimeOnly group: 'io.spiffe', name: 'grpc-netty-macos', version: '0.8.12'
```
## SPIFFE SVID based KeyStore and TrustStore Provider
#### Aarch64 (M1) Architecture
### Install the SPIFFE Provider JAR
If you are running the aarch64 architecture (M1 CPUs), add to your `pom.xml`:
Generate the JAR that includes all dependencies:
```xml
```
./gradlew build
<dependency>
<groupId>io.spiffe</groupId>
<artifactId>grpc-netty-macos-aarch64</artifactId>
<version>0.8.12</version>
<scope>runtime</scope>
</dependency>
```
For installing the JAR file containing the provider classes as a bundled extension in the java platform, copy
`build/libs/spiffe-provider-<version>-all.jar` to `<java-home>/jre/lib/ext`
Using Gradle:
### Configure `java.security`
Java Security Providers are configured in the master security properties file `<java-home>/jre/lib/security/java.security`.
The way to register a provider is to specify the Provider subclass name and priority in the format
```
security.provider.<n>=<className>
```gradle
runtimeOnly group: 'io.spiffe', name: 'grpc-netty-macos-aarch64', version: '0.8.12'
```
This declares a provider, and specifies its preference order n.
*Caveat: not all OpenJDK distributions are aarch64 native, make sure your JDK is also running
natively*
#### Register the SPIFFE Provider
You can extend and override the master security properties file.
## Java SPIFFE Helper
Create a file `java.security` with the following content:
The `java-spiffe-helper` module manages X.509 SVIDs and Bundles in Java Keystores.
### Docker Image
Pull the `java-spiffe-helper` image from `ghcr.io/spiffe/java-spiffe-helper:0.8.12`.
For more details, see [java-spiffe-helper/README.md](java-spiffe-helper/README.md).
## Build the JARs
On Linux or MacOS, run:
```
security.provider.<n>=spiffe.provider.SpiffeProvider
# Determines the default key and trust manager factory algorithms for
# the javax.net.ssl package.
ssl.KeyManagerFactory.algorithm=Spiffe
ssl.TrustManagerFactory.algorithm=Spiffe
# The list of spiffeIDs that will be authorized
ssl.spiffe.accept=spiffe://example.org/workload, spiffe://example.org/workload2, spiffe://example2.org/workload
$ ./gradlew assemble
BUILD SUCCESSFUL
```
In your `java.security` file:
All `jar` files are placed in `build/libs` folder.
* replace `<n>` following the order of the `# List of Providers` in the master file.
#### Jars that include all dependencies
* replace the value of the custom property `ssl.spiffe.accept` with the Spiffe IDs of the workloads that are allowed to connect.
If the property is not present or if it's empty, any spiffe id will be authorized.
For the module [java-spiffe-provider](java-spiffe-provider), a fat jar is generated with the classifier `-all-[os-classifier]`.
To pass your custom security properties file through the command line via system property when starting the JVM:
For the module [java-spiffe-helper](java-spiffe-helper), a fat jar is generated with the classifier `[os-classifier]`.
```
-Djava.security.properties=<path to java.security>
```
Based on the OS where the build is run, the `[os-classifier]` will be:
For example, it can be passed in the `JAVA_OPTS` used by the Tomcat's startup script:
```
$ export JAVA_OPTS="$JAVA_OPTS -Djava.security.properties=java.security"
$ ./catalina.sh run
```
The properties defined in your custom properties file will override the properties in the master file.
### Configure Workload API Socket Endpoint
The socket endpoint can be configured defining an environment variable named `SPIFFE_ENDPOINT_SOCKET`:
```
export SPIFFE_ENDPOINT_SOCKET=/tmp/agent.sock
```
or it can be configured from the command line via system property:
```
-Dspiffe.endpoint.socket=/tmp/agent.sock
```
If both are defined, system property overrules.
If the endpoint socket is not defined, there will be an error stating `SPIFFE_ENDPOINT_SOCKET is not defined`.
### Configure a Tomcat connector
A Tomcat TLS connector that uses the `Spiffe` KeyStore can be configured as follows:
```
<Connector
protocol="org.apache.coyote.http11.Http11NioProtocol"
port="8443" maxThreads="200"
scheme="https" secure="true" SSLEnabled="true"
keystoreFile="" keystorePass=""
keystoreType="Spiffe"
clientAuth="true" sslProtocol="TLS"/>
```
## Running Demos
A running example using the SPIFFE Provider in Tomcat is available on [java-spiffe-example](https://github.com/spiffe/spiffe-example/tree/master/java-keystore-tomcat-demo)
A demo that shows Federation and TCP support is available on [java-spiffe-federation-jboss](https://github.com/spiffe/spiffe-example/tree/master/java-spiffe-federation-jboss)
## References
[How to Implement a Provider in the Java Cryptography Architecture](https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/HowToImplAProvider.html)
[Java PKI Programmer's Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/security/certpath/CertPathProgGuide.html)
* `-linux-x86_64` for Linux
* `-osx-x86_64` for MacOS with x86_64 architecture
* `-osx-aarch64` for MacOS with aarch64 architecture (M1)

View File

@ -1,77 +1,181 @@
group 'spiffe'
version '0.5.0'
plugins {
id 'com.github.kt3k.coveralls' version '2.12.2'
id 'com.google.osdetector' version '1.7.3'
id 'jvm-test-suite'
}
buildscript {
allprojects {
repositories {
mavenCentral()
jcenter()
}
apply plugin: 'jacoco'
}
subprojects {
group = 'io.spiffe'
version = project.version
ext {
grpcVersion = '1.73.0'
jupiterVersion = '5.13.2'
mockitoVersion = '4.11.0'
lombokVersion = '1.18.38'
nimbusVersion = '10.3.1'
shadowVersion = '8.1.1'
//IMPORTANT: This must be in sync with the shaded netty version in gRPC
nettyVersion = '4.2.2.Final'
}
ext.protobufPluginVersion = '0.8.7'
ext.shadowPluginVersion = '4.0.3'
apply plugin: 'java-library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
}
javadoc {
exclude "**/grpc/**"
exclude "**/internal/**"
}
publishing {
repositories {
maven {
credentials {
username = project.properties["mavenDeployUser"] ?: System.getenv("NEXUS_USERNAME")
password = project.properties["mavenDeployPassword"] ?: System.getenv("NEXUS_TOKEN")
}
url = project.properties["mavenDeployUrl"]
}
}
publications {
mavenJava(MavenPublication) {
groupId project.group
version "${project.version}"
from components.java
pom {
name = project.name
artifactId = project.name
url = 'https://github.com/spiffe/java-spiffe'
afterEvaluate {
// description is not available until evaluated.
description = project.description
}
scm {
connection = 'scm:git:https://github.com/spiffe/java-spiffe.git'
developerConnection = 'scm:git:git@github.com:spiffe/java-spiffe.git'
url = 'https://github.com/spiffe/java-spiffe'
}
licenses {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
['maxlambrecht:Max Lambrecht', 'rturner3:Ryan Turner'].each { devData ->
developer {
def devInfo = devData.split(':')
id = devInfo[0]
name = devInfo[1]
url = 'https://github.com/' + devInfo[0]
roles = ["Maintainer"]
}
}
}
}
}
}
}
signing {
useInMemoryPgpKeys(System.getenv('PGP_PRIVATE_KEY'), System.getenv('PGP_KEY_PASSPHRASE'))
sign publishing.publications.mavenJava
}
dependencies {
classpath group: 'com.google.protobuf', name: 'protobuf-gradle-plugin', version: "${protobufPluginVersion}"
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: "${shadowPluginVersion}"
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.17.0'
implementation group: 'commons-validator', name: 'commons-validator', version: "1.9.0"
testCompileOnly group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: "${jupiterVersion}"
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: "${jupiterVersion}"
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: "${jupiterVersion}"
testCompileOnly group: 'org.mockito', name: 'mockito-core', version: "${mockitoVersion}"
testRuntimeOnly group: 'org.mockito', name: 'mockito-junit-jupiter', version: "${mockitoVersion}"
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
testImplementation group: 'uk.org.webcompere', name: 'system-stubs-core', version: '2.0.3' // Last version supporting Java 8
} else {
testImplementation group: 'uk.org.webcompere', name: 'system-stubs-core', version: '2.1.8'
}
// Project Lombok dependency
compileOnly group: 'org.projectlombok', name: 'lombok', version: "${lombokVersion}"
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: "${lombokVersion}"
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: "${lombokVersion}"
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: "${lombokVersion}"
}
}
ext {
grpcVersion = '1.21.0'
nettyVersion = '4.1.33.Final'
protobufProtocVersion = '3.7.1'
apacheCommonsVersion = '3.8.1'
}
apply plugin: 'java-library'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.google.protobuf'
assemble.dependsOn shadowJar
shadowJar {
classifier = "all"
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
testing {
suites {
test {
useJUnitJupiter()
}
}
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufProtocVersion}"
task jacocoTestReport(type: JacocoReport) {
// Gather execution data from all subprojects
executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec")
// Add all relevant sourcesets from the subprojects
subprojects.each {
sourceSets it.sourceSets.main
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
// Filter out autogenerated or internal code
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**', '**/internal/**'])
}))
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
reports {
xml.required = true
html.required = true
}
}
repositories {
mavenCentral()
jacocoTestReport.dependsOn {
subprojects.collectMany { project ->
project.tasks.matching { it.name in ['test'] }
}
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation group: 'io.grpc', name: 'grpc-netty', version: "${grpcVersion}"
implementation group: 'io.grpc', name: 'grpc-protobuf', version: "${grpcVersion}"
implementation group: 'io.grpc', name: 'grpc-stub', version: "${grpcVersion}"
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: "${nettyVersion}", classifier: 'linux-x86_64'
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: "${nettyVersion}", classifier: 'osx-x86_64'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: "${apacheCommonsVersion}"
compileOnly group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'
coveralls {
jacocoReportPath 'build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml'
sourceDirs = ['java-spiffe-core/src/main/java',
'java-spiffe-helper/src/main/java',
'java-spiffe-provider/src/main/java']
}
// copy submodules jars to a common folder for deploy
task copyJars(type: Copy) {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from subprojects.collect { it.tasks.withType(Jar) }
into "$buildDir/libs"
}
assemble.finalizedBy copyJars

View File

@ -0,0 +1,25 @@
# Example java-spiffe-helper configuration
# KeyStore Path
keyStorePath = keystore.p12
# Password for the KeyStore
keyStorePass = REPLACE_WITH_YOUR_KEYSTORE_PASSWORD
# Password for the private key within the KeyStore
keyPass = REPLACE_WITH_YOUR_PRIVATE_KEY_PASSWORD
# Path to the TrustStore file
trustStorePath = truststore.p12
# TrustStore Password: Password for the TrustStore
trustStorePass = REPLACE_WITH_YOUR_TRUSTSTORE_PASSWORD
# KeyStore Type: 'pkcs12' (default) or 'jks'
keyStoreType = pkcs12
# Key Alias: Alias of the key within the KeyStore (Default: `spiffe`)
keyAlias = spiffe
# SPIFFE Socket Path: Path to the SPIRE Agent's public API socket
spiffeSocketPath = unix:/tmp/spire-agent/public/api.sock

2
gradle.properties Normal file
View File

@ -0,0 +1,2 @@
version=0.8.12
mavenDeployUrl=https://oss.sonatype.org/service/local/staging/deploy/maven2

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Mon May 21 14:01:33 ART 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip

303
gradlew vendored
View File

@ -1,78 +1,126 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright © 2015-2021 the original 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -81,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -89,84 +137,109 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

56
gradlew.bat vendored
View File

@ -1,4 +1,20 @@
@if "%DEBUG%" == "" @echo off
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -9,19 +25,23 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +55,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,38 +65,26 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

109
java-spiffe-core/README.md Normal file
View File

@ -0,0 +1,109 @@
# JAVA-SPIFFE Core
Core functionality to fetch, process and validate X.509 and JWT SVIDs and Bundles from the Workload API.
It uses the [JWT and JOSE Nimbus Library](https://connect2id.com/products/nimbus-jose-jwt) to parse and process the JWT tokens
and JSON Web Key (JWK) set bundles.
## X.509 Source
A `X509Source` represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API.
To create a new X.509 Source:
```
X509Source x509Source;
try {
x509Source = DefaultX509Source.newSource();
} catch (SocketEndpointAddressException | X509SourceException e) {
// handle exception
}
X509Svid svid = x509Source.getX509Svid();
X509Bundle bundle = x509Source.getBundleForTrustDomain(TrustDomain.of("example.org"));
```
The `newSource()` method blocks until the X.509 materials can be retrieved from the Workload API and the `X509Source` is
initialized with the X.509 SVIDs and Bundles. A `X.509 context watcher` is configured on the `X509Source` to automatically get
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
reports an error.
The socket endpoint address is configured through the environment variable `SPIFFE_ENDPOINT_SOCKET`. Another way to
configure it is by providing an `X509SourceOptions` instance to the `newSource` method:
```
DefaultX509Source.X509SourceOptions x509SourceOptions = DefaultX509Source.X509SourceOptions
.builder()
.spiffeSocketPath("unix:/tmp/agent-other.sock")
.svidPicker(list -> list.get(list.size()-1))
.build();
X509Source x509Source = DefaultX509Source.newSource(x509SourceOptions);
```
It allows to configure another SVID picker. By default, the first SVID is used.
### Configure a timeout for X509Source initialization
The `X509Source newSource()` method blocks waiting until an X.509 context is fetched. The X.509 context fetch is retried
using an exponential backoff policy with this progression of delays between retries: 1 second, 2 seconds, 4, 8, 16, 32, 60, 60, 60...
It retries indefinitely unless a timeout is configured.
This timeout can be configured either providing it through the `newSource(Duration timeout)` method or
using a System property:
`spiffe.newX509Source.timeout=PT30S`
The `timeout` duration is expressed in `ISO-8601` format.
## JWT Source
A `JwtSource` represents a source of JWT SVIDs and bundles maintained via the Workload API.
To create a new JWT Source:
```
JwtSource jwtSource;
try {
jwtSource = JwtSource.newSource();
} catch (SocketEndpointAddressException | JwtSourceException e) {
// handle exception
}
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/test"), "testaudience1", "audience2");
JwtBundle bundle = jwtSource.getBundleForTrustDomain(TrustDomain.of("example.org"));
```
The `newSource()` method blocks until the JWT materials can be retrieved from the Workload API and the `JwtSource` is
initialized with the JWT Bundles. A `JWT context watcher` is configured on the JwtSource to automatically get
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
reports an error.
The socket endpoint address is configured through the environment variable `SPIFFE_ENDPOINT_SOCKET`.
Another way to configure it is by providing an `JwtSourceOptions` instance to the `newSource` method:
```
JwtSource.JwtSourceOptions jwtSourceOptions = JwtSource.JwtSourceOptions
.builder()
.spiffeSocketPath("unix:/tmp/agent-other.sock")
.build();
JwtSource jwtSource = JwtSource.newSource(jwtSourceOptions);
```
### Configure a timeout for JwtSource initialization
The `JwtSource newSource()` method blocks until the JWT materials are fetched. The fetching process is retried
using an exponential backoff policy with this progression of delays between retries: 1 second, 2 seconds, 4, 8, 16, 32, 60, 60, 60...
It retries indefinitely unless a timeout is configured.
This timeout can be configured either providing it through the `newSource(Duration timeout)` method or
using a System property:
`spiffe.newJwtSource.timeout=PT30S`
The `timeout` duration is expressed in `ISO-8601` format.

View File

@ -0,0 +1,101 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'com.google.protobuf', name: 'protobuf-gradle-plugin', version: '0.9.5'
}
}
description = "Core functionality to fetch, process and validate X.509 and JWT SVIDs and Bundles from the Workload API."
apply plugin: 'com.google.protobuf'
apply plugin: 'java-test-fixtures'
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
integrationTest {
java {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integrationTest/java')
}
resources.srcDir file('src/integrationTest/resources')
}
}
sourcesJar.duplicatesStrategy = DuplicatesStrategy.INCLUDE
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestCompile.extendsFrom testCompile
integrationTestCompileOnly.extendsFrom testCompileOnly
integrationTestRuntime.extendsFrom testRuntime
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
task integrationTest(type: Test) {
useJUnitPlatform()
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
outputs.upToDateWhen { false }
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.25.5'
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
dependencies {
if (osdetector.os.is('osx') ) {
project.ext.osArch = System.getProperty("os.arch")
if ("x86_64" == project.ext.osArch) {
compileOnly(project('grpc-netty-macos'))
testImplementation(project('grpc-netty-macos'))
} else if ("aarch64" == project.ext.osArch) {
compileOnly(project('grpc-netty-macos-aarch64'))
testImplementation(project('grpc-netty-macos-aarch64'))
} else {
throw new GradleException("Architecture not supported: " + project.ext.osArch)
}
} else {
compileOnly(project('grpc-netty-linux'))
testImplementation(project('grpc-netty-linux'))
}
project.ext.osArch = System.getProperty("os.arch")
implementation group: 'io.grpc', name: 'grpc-protobuf', version: "${grpcVersion}"
implementation group: 'io.grpc', name: 'grpc-stub', version: "${grpcVersion}"
testImplementation group: 'io.grpc', name: 'grpc-inprocess', version: "${grpcVersion}"
testImplementation group: 'io.grpc', name: 'grpc-testing', version: "${grpcVersion}"
compileOnly group: 'org.apache.tomcat', name: 'annotations-api', version: '6.0.53' // necessary for Java 9+
// library for processing JWT tokens and JOSE JWK bundles
implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: "${nimbusVersion}"
testFixturesImplementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: "${nimbusVersion}"
// using bouncy castle for generating X.509 certs for testing purposes
testFixturesImplementation group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.70'
testFixturesImplementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.17.0'
}

View File

@ -0,0 +1,9 @@
description = "Java SPIFFE Library GRPC-Netty Linux support module"
dependencies {
implementation group: 'io.grpc', name: 'grpc-netty-shaded', version: "${grpcVersion}"
}
jar {
archiveClassifier = ""
}

View File

@ -0,0 +1,81 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.NegotiationType;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.channel.ChannelOption;
import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup;
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollDomainSocketChannel;
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress;
import lombok.NonNull;
import lombok.val;
import org.apache.commons.lang3.SystemUtils;
import java.net.URI;
import java.util.concurrent.ExecutorService;
/**
* Factory for creating ManagedChannel instances for Linux OS.
*/
public final class GrpcManagedChannelFactory {
private static final String UNIX_SCHEME = "unix";
private static final String TCP_SCHEME = "tcp";
private GrpcManagedChannelFactory() {
}
/**
* Returns a ManagedChannelWrapper that contains a {@link ManagedChannel} to the SPIFFE Socket Endpoint provided.
*
* @param address URI representing the Workload API endpoint.
* @param executorService the executor to configure the event loop group
* @return a instance of a {@link ManagedChannelWrapper}
*/
public static ManagedChannelWrapper newChannel(@NonNull URI address, ExecutorService executorService) {
val scheme = address.getScheme();
ManagedChannelWrapper result;
switch (scheme) {
case UNIX_SCHEME:
result = createNativeSocketChannel(address, executorService);
break;
case TCP_SCHEME:
result = createTcpChannel(address);
break;
default:
throw new IllegalArgumentException("Address Scheme not supported: ");
}
return result;
}
// Create a Native Socket Channel pointing to the spiffeSocketPath
private static ManagedChannelWrapper createNativeSocketChannel(@NonNull URI address, ExecutorService executorService) {
NettyChannelBuilder channelBuilder = NettyChannelBuilder.
forAddress(new DomainSocketAddress(address.getPath()));
EventLoopGroup eventLoopGroup = configureNativeSocketChannel(channelBuilder, executorService);
ManagedChannel managedChannel = channelBuilder.usePlaintext().build();
return new ManagedChannelWrapper(managedChannel, eventLoopGroup);
}
private static ManagedChannelWrapper createTcpChannel(@NonNull URI address) {
ManagedChannel managedChannel = NettyChannelBuilder.forAddress(address.getHost(), address.getPort())
.negotiationType(NegotiationType.PLAINTEXT)
.build();
return new ManagedChannelWrapper(managedChannel);
}
private static EventLoopGroup configureNativeSocketChannel(@NonNull NettyChannelBuilder channelBuilder, ExecutorService executorService) {
if (SystemUtils.IS_OS_LINUX) {
// nThreads = 0 -> use Netty default
EpollEventLoopGroup epollEventLoopGroup = new EpollEventLoopGroup(0, executorService);
channelBuilder.eventLoopGroup(epollEventLoopGroup)
// avoid warning Unknown channel option 'SO_KEEPALIVE'
.withOption(ChannelOption.SO_KEEPALIVE, null)
.channelType(EpollDomainSocketChannel.class);
return epollEventLoopGroup;
}
throw new IllegalStateException("Operating System is not supported.");
}
}

View File

@ -0,0 +1,51 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup;
import java.io.Closeable;
/**
* Wraps a {@link ManagedChannel} along with the {@link EventLoopGroup} in order to
* have more control and be able to shutdown the channel properly
* calling the shutdownGracefully method on the EventLoopGroup to prevent
* that some threads remain active.
*/
public class ManagedChannelWrapper implements Closeable {
private final ManagedChannel managedChannel;
private final EventLoopGroup eventLoopGroup;
/**
* Constructor.
*
* @param managedChannel an instance of {@link ManagedChannel}
* @param eventLoopGroup an instance of {@link EventLoopGroup}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel, EventLoopGroup eventLoopGroup) {
this.managedChannel = managedChannel;
this.eventLoopGroup = eventLoopGroup;
}
/**
* Constructor.
*
* @param managedChannel a {@link ManagedChannel}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel) {
this.managedChannel = managedChannel;
this.eventLoopGroup = null;
}
@Override
public void close() {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
managedChannel.shutdown();
}
public ManagedChannel getChannel() {
return managedChannel;
}
}

View File

@ -0,0 +1,12 @@
description = "Java SPIFFE Library GRPC-Netty MacOS module"
dependencies {
implementation group: 'io.grpc', name: 'grpc-netty', version: "${grpcVersion}"
// version must match the one in grpc-netty
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: "${nettyVersion}", classifier: 'osx-aarch_64'
}
jar {
archiveClassifier = ""
}

View File

@ -0,0 +1,81 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.kqueue.KQueueDomainSocketChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.unix.DomainSocketAddress;
import lombok.NonNull;
import lombok.val;
import org.apache.commons.lang3.SystemUtils;
import java.net.URI;
import java.util.concurrent.ExecutorService;
/**
* Factory for creating ManagedChannel instances for Mac OS.
*/
public final class GrpcManagedChannelFactory {
private static final String UNIX_SCHEME = "unix";
private static final String TCP_SCHEME = "tcp";
private GrpcManagedChannelFactory() {
}
/**
* Returns a ManagedChannelWrapper that contains a {@link ManagedChannel} to the SPIFFE Socket Endpoint provided.
*
* @param address URI representing the Workload API endpoint.
* @param executorService the executor to configure the event loop group
* @return a instance of a {@link ManagedChannelWrapper}
*/
public static ManagedChannelWrapper newChannel(@NonNull URI address, ExecutorService executorService) {
val scheme = address.getScheme();
ManagedChannelWrapper result;
switch (scheme) {
case UNIX_SCHEME:
result = createNativeSocketChannel(address, executorService);
break;
case TCP_SCHEME:
result = createTcpChannel(address);
break;
default:
throw new IllegalArgumentException("Address Scheme not supported: ");
}
return result;
}
// Create a Native Socket Channel pointing to the spiffeSocketPath
private static ManagedChannelWrapper createNativeSocketChannel(@NonNull URI address, ExecutorService executorService) {
NettyChannelBuilder channelBuilder = NettyChannelBuilder.
forAddress(new DomainSocketAddress(address.getPath()));
EventLoopGroup eventLoopGroup = configureNativeSocketChannel(channelBuilder, executorService);
ManagedChannel managedChannel = channelBuilder.usePlaintext().build();
return new ManagedChannelWrapper(managedChannel, eventLoopGroup);
}
private static ManagedChannelWrapper createTcpChannel(@NonNull URI address) {
ManagedChannel managedChannel = NettyChannelBuilder.forAddress(address.getHost(), address.getPort())
.negotiationType(NegotiationType.PLAINTEXT)
.build();
return new ManagedChannelWrapper(managedChannel);
}
private static EventLoopGroup configureNativeSocketChannel(@NonNull NettyChannelBuilder channelBuilder, ExecutorService executorService) {
if (SystemUtils.IS_OS_MAC) {
// nThreads = 0 -> use Netty default
KQueueEventLoopGroup eventLoopGroup = new KQueueEventLoopGroup(0, executorService);
channelBuilder.eventLoopGroup(eventLoopGroup)
// avoid warning Unknown channel option 'SO_KEEPALIVE'
.withOption(ChannelOption.SO_KEEPALIVE, null)
.channelType(KQueueDomainSocketChannel.class);
return eventLoopGroup;
}
throw new IllegalStateException("Operating System is not supported.");
}
}

View File

@ -0,0 +1,51 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.netty.channel.EventLoopGroup;
import java.io.Closeable;
/**
* Wraps a {@link ManagedChannel} along with the {@link EventLoopGroup} in order to
* have more control and be able to shutdown the channel properly
* calling the shutdownGracefully method on the EventLoopGroup to prevent
* that some threads remain active.
*/
public class ManagedChannelWrapper implements Closeable {
private final ManagedChannel managedChannel;
private final EventLoopGroup eventLoopGroup;
/**
* Constructor.
*
* @param managedChannel an instance of {@link ManagedChannel}
* @param eventLoopGroup an instance of {@link EventLoopGroup}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel, EventLoopGroup eventLoopGroup) {
this.managedChannel = managedChannel;
this.eventLoopGroup = eventLoopGroup;
}
/**
* Constructor.
*
* @param managedChannel a {@link ManagedChannel}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel) {
this.managedChannel = managedChannel;
this.eventLoopGroup = null;
}
@Override
public void close() {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
managedChannel.shutdown();
}
public ManagedChannel getChannel() {
return managedChannel;
}
}

View File

@ -0,0 +1,10 @@
description = "Java SPIFFE Library GRPC-Netty MacOS module"
dependencies {
implementation group: 'io.grpc', name: 'grpc-netty', version: "${grpcVersion}"
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: "${nettyVersion}", classifier: 'osx-x86_64'
}
jar {
archiveClassifier = ""
}

View File

@ -0,0 +1,81 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.kqueue.KQueueDomainSocketChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.unix.DomainSocketAddress;
import lombok.NonNull;
import lombok.val;
import org.apache.commons.lang3.SystemUtils;
import java.net.URI;
import java.util.concurrent.ExecutorService;
/**
* Factory for creating ManagedChannel instances for Mac OS.
*/
public final class GrpcManagedChannelFactory {
private static final String UNIX_SCHEME = "unix";
private static final String TCP_SCHEME = "tcp";
private GrpcManagedChannelFactory() {
}
/**
* Returns a ManagedChannelWrapper that contains a {@link ManagedChannel} to the SPIFFE Socket Endpoint provided.
*
* @param address URI representing the Workload API endpoint.
* @param executorService the executor to configure the event loop group
* @return a instance of a {@link ManagedChannelWrapper}
*/
public static ManagedChannelWrapper newChannel(@NonNull URI address, ExecutorService executorService) {
val scheme = address.getScheme();
ManagedChannelWrapper result;
switch (scheme) {
case UNIX_SCHEME:
result = createNativeSocketChannel(address, executorService);
break;
case TCP_SCHEME:
result = createTcpChannel(address);
break;
default:
throw new IllegalArgumentException("Address Scheme not supported: ");
}
return result;
}
// Create a Native Socket Channel pointing to the spiffeSocketPath
private static ManagedChannelWrapper createNativeSocketChannel(@NonNull URI address, ExecutorService executorService) {
NettyChannelBuilder channelBuilder = NettyChannelBuilder.
forAddress(new DomainSocketAddress(address.getPath()));
EventLoopGroup eventLoopGroup = configureNativeSocketChannel(channelBuilder, executorService);
ManagedChannel managedChannel = channelBuilder.usePlaintext().build();
return new ManagedChannelWrapper(managedChannel, eventLoopGroup);
}
private static ManagedChannelWrapper createTcpChannel(@NonNull URI address) {
ManagedChannel managedChannel = NettyChannelBuilder.forAddress(address.getHost(), address.getPort())
.negotiationType(NegotiationType.PLAINTEXT)
.build();
return new ManagedChannelWrapper(managedChannel);
}
private static EventLoopGroup configureNativeSocketChannel(@NonNull NettyChannelBuilder channelBuilder, ExecutorService executorService) {
if (SystemUtils.IS_OS_MAC) {
// nThreads = 0 -> use Netty default
KQueueEventLoopGroup eventLoopGroup = new KQueueEventLoopGroup(0, executorService);
channelBuilder.eventLoopGroup(eventLoopGroup)
// avoid warning Unknown channel option 'SO_KEEPALIVE'
.withOption(ChannelOption.SO_KEEPALIVE, null)
.channelType(KQueueDomainSocketChannel.class);
return eventLoopGroup;
}
throw new IllegalStateException("Operating System is not supported.");
}
}

View File

@ -0,0 +1,51 @@
package io.spiffe.workloadapi.internal;
import io.grpc.ManagedChannel;
import io.netty.channel.EventLoopGroup;
import java.io.Closeable;
/**
* Wraps a {@link ManagedChannel} along with the {@link EventLoopGroup} in order to
* have more control and be able to shutdown the channel properly
* calling the shutdownGracefully method on the EventLoopGroup to prevent
* that some threads remain active.
*/
public class ManagedChannelWrapper implements Closeable {
private final ManagedChannel managedChannel;
private final EventLoopGroup eventLoopGroup;
/**
* Constructor.
*
* @param managedChannel an instance of {@link ManagedChannel}
* @param eventLoopGroup an instance of {@link EventLoopGroup}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel, EventLoopGroup eventLoopGroup) {
this.managedChannel = managedChannel;
this.eventLoopGroup = eventLoopGroup;
}
/**
* Constructor.
*
* @param managedChannel a {@link ManagedChannel}
*/
public ManagedChannelWrapper(ManagedChannel managedChannel) {
this.managedChannel = managedChannel;
this.eventLoopGroup = null;
}
@Override
public void close() {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
managedChannel.shutdown();
}
public ManagedChannel getChannel() {
return managedChannel;
}
}

View File

@ -0,0 +1,79 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.exception.X509ContextException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.jwtsvid.JwtSvid;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
// To run these tests there should be a Workload API running, the SPIFFE_ENDPOINT_SOCKET env variable should be defined,
// and there should be a registration entry with a SPIFFE-ID = 'spiffe://example.org/myservice' and a selector unix:uid with
// the user id used to run the process.
class WorkloadApiIntegrationTest {
private WorkloadApiClient client;
@BeforeEach
void setup() throws SocketEndpointAddressException {
client = DefaultWorkloadApiClient.newClient();
}
@Test
void testFetchX509Context() throws X509ContextException, BundleNotFoundException {
X509Context response = client.fetchX509Context();
Assertions.assertEquals(response.getDefaultSvid().getSpiffeId(), SpiffeId.parse("spiffe://example.org/myservice"));
Assertions.assertNotNull(response.getX509BundleSet().getBundleForTrustDomain(TrustDomain.parse("example.org")));
}
@Test
void testFetchJwtBundles() throws BundleNotFoundException, JwtBundleException {
JwtBundleSet response = client.fetchJwtBundles();
Assertions.assertNotNull(response.getBundleForTrustDomain(TrustDomain.parse("example.org")));
}
@Test
void testFetchX509Bundles() throws BundleNotFoundException, X509BundleException {
X509BundleSet response = client.fetchX509Bundles();
Assertions.assertNotNull(response.getBundleForTrustDomain(TrustDomain.parse("example.org")));
}
@Test
void testFetchJwtSvid() throws JwtSvidException {
JwtSvid response = client.fetchJwtSvid("audience1", "audience2");
Assertions.assertEquals(response.getSpiffeId(), SpiffeId.parse("spiffe://example.org/myservice"));
Assertions.assertTrue(response.getAudience().contains("audience1"));
Assertions.assertTrue(response.getAudience().contains("audience2"));
Assertions.assertNotNull(response.getToken());
}
@Test
void testValidateJwtSvid() throws JwtSvidException {
String token = client.fetchJwtSvid("audience1", "audience2").getToken();
JwtSvid response = client.validateJwtSvid(token, "audience1");
Assertions.assertEquals(response.getSpiffeId(), SpiffeId.parse("spiffe://example.org/myservice"));
Assertions.assertTrue(response.getAudience().contains("audience1"));
Assertions.assertTrue(response.getAudience().contains("audience2"));
}
@Test
void testValidateJwtSvid_invalid_audience() throws JwtSvidException {
String token = client.fetchJwtSvid("audience1", "audience2").getToken();
try {
client.validateJwtSvid(token, "other");
Assertions.fail();
} catch (JwtSvidException e) {
Assertions.assertEquals("Error validating JWT SVID", e.getMessage());
}
}
}

View File

@ -0,0 +1,21 @@
package io.spiffe.bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.spiffeid.TrustDomain;
import lombok.NonNull;
/**
* Represents a source of bundles of type T keyed by trust domain.
*/
public interface BundleSource<T> {
/**
* Returns the bundle of type T associated to the given trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return the a bundle of type T for the given trust domain
* @throws BundleNotFoundException if no bundle is found for the given trust domain
*/
T getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException;
}

View File

@ -0,0 +1,207 @@
package io.spiffe.bundle.jwtbundle;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import io.spiffe.internal.JwtSignatureAlgorithm;
import io.spiffe.bundle.BundleSource;
import io.spiffe.exception.AuthorityNotFoundException;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.spiffeid.TrustDomain;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyException;
import java.security.PublicKey;
import java.text.ParseException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents a collection of trusted JWT authorities (Public Keys) for a trust domain.
*/
@Value
public class JwtBundle implements BundleSource<JwtBundle> {
TrustDomain trustDomain;
Map<String, PublicKey> jwtAuthorities;
/**
* Creates a new JWT bundle for a trust domain.
*
* @param trustDomain a {@link TrustDomain} to associate to the JwtBundle
*/
public JwtBundle(@NonNull final TrustDomain trustDomain) {
this.trustDomain = trustDomain;
this.jwtAuthorities = new ConcurrentHashMap<>();
}
/**
* Creates a new JWT bundle for a trust domain with JWT Authorities (public keys associated to keyIds).
*
* @param trustDomain a {@link TrustDomain} to associate to the JwtBundle
* @param jwtAuthorities a Map of public Keys
*/
public JwtBundle(@NonNull final TrustDomain trustDomain, @NonNull final Map<String, PublicKey> jwtAuthorities) {
this.trustDomain = trustDomain;
this.jwtAuthorities = new ConcurrentHashMap<>(jwtAuthorities);
}
/**
* Loads a JWT bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document.
* <p>
* Key Types supported are EC and RSA.
*
* @param trustDomain a {@link TrustDomain} to associate to the JWT bundle.
* @param bundlePath a path to a file containing the JWT authorities (public keys).
* @return an instance of a {@link JwtBundle}
* @throws JwtBundleException if there is an error reading or parsing the file, or if a keyId is empty
* @throws KeyException if the bundle file contains a key type that is not supported
*/
public static JwtBundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath)
throws KeyException, JwtBundleException {
try {
val jwkSet = JWKSet.load(bundlePath.toFile());
return toJwtBundle(trustDomain, jwkSet);
} catch (IllegalArgumentException | IOException | ParseException | JOSEException e) {
val error = "Could not load bundle from file: %s";
throw new JwtBundleException(String.format(error, bundlePath.toString()), e);
}
}
/**
* Parses a JWT bundle from a byte array.
*
* @param trustDomain a {@link TrustDomain}
* @param bundleBytes an array of bytes representing the JWT bundle.
* @return an instance of a {@link JwtBundle}
* @throws JwtBundleException if there is an error reading or parsing the file, or if a keyId is empty
*/
public static JwtBundle parse(
@NonNull final TrustDomain trustDomain,
@NonNull final byte[] bundleBytes)
throws JwtBundleException {
try {
val jwkSet = JWKSet.parse(new String(bundleBytes));
return toJwtBundle(trustDomain, jwkSet);
} catch (ParseException | JOSEException | KeyException e) {
throw new JwtBundleException("Could not parse bundle from bytes", e);
}
}
/**
* Returns the JWT bundle for a trust domain.
*
* @param trustDomain a {@link TrustDomain}
* @return a {@link JwtBundle} for the trust domain
* @throws BundleNotFoundException if there is no bundle for the given trust domain
*/
@Override
public JwtBundle getBundleForTrustDomain(final TrustDomain trustDomain) throws BundleNotFoundException {
if (this.trustDomain.equals(trustDomain)) {
return this;
}
throw new BundleNotFoundException(String.format("No JWT bundle found for trust domain %s", trustDomain));
}
/**
* Returns the JWT authorities in the bundle, keyed by key ID.
*
* @return the JWT authorities in the bundle, keyed by key ID
*/
public Map<String, PublicKey> getJwtAuthorities() {
return Collections.unmodifiableMap(jwtAuthorities);
}
/**
* Finds the JWT key with the given key id from the bundle.
*
* @param keyId the Key ID
* @return {@link PublicKey} representing the Authority associated to the KeyID.
* @throws AuthorityNotFoundException if no Authority is found associated to the Key ID
*/
public PublicKey findJwtAuthority(final String keyId) throws AuthorityNotFoundException {
val key = jwtAuthorities.get(keyId);
if (key != null) {
return key;
}
throw new AuthorityNotFoundException(String.format("No authority found for the trust domain %s and key id %s", this.trustDomain, keyId));
}
/**
* Looks for a JWT authority id in the JWT bundle.
*
* @param keyId id of a JWT Authority
* @return true if the bundle has a JWT authority with the given key ID.
*/
public boolean hasJwtAuthority(final String keyId) {
return jwtAuthorities.containsKey(keyId);
}
/**
* 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.
*
* @param keyId Key ID to associate to the jwtAuthority
* @param jwtAuthority a PublicKey
*/
public void putJwtAuthority(@NonNull final String keyId, @NonNull final PublicKey jwtAuthority) {
if (StringUtils.isBlank(keyId)) {
throw new IllegalArgumentException("KeyId cannot be empty");
}
jwtAuthorities.put(keyId, jwtAuthority);
}
/**
* Removes the JWT authority identified by the key ID from the bundle.
*
* @param keyId The key id of the JWT authority to be removed
*/
public void removeJwtAuthority(final String keyId) {
jwtAuthorities.remove(keyId);
}
private static JwtBundle toJwtBundle(final TrustDomain trustDomain, final JWKSet jwkSet) throws JwtBundleException, JOSEException, ParseException, KeyException {
final Map<String, PublicKey> authorities = new ConcurrentHashMap<>();
for (JWK jwk : jwkSet.getKeys()) {
String keyId = getKeyId(jwk);
PublicKey publicKey = getPublicKey(jwk);
authorities.put(keyId, publicKey);
}
return new JwtBundle(trustDomain, authorities);
}
private static String getKeyId(final JWK jwk) throws JwtBundleException {
val keyId = jwk.getKeyID();
if (StringUtils.isBlank(keyId)) {
throw new JwtBundleException("Error adding authority of JWKS: keyID cannot be empty");
}
return keyId;
}
private static PublicKey getPublicKey(final JWK jwk) throws JOSEException, ParseException, KeyException {
val family = JwtSignatureAlgorithm.Family.parse(jwk.getKeyType().getValue());
final PublicKey publicKey;
switch (family) {
case EC:
publicKey = ECKey.parse(jwk.toJSONString()).toPublicKey();
break;
case RSA:
publicKey = RSAKey.parse(jwk.toJSONString()).toPublicKey();
break;
default:
throw new KeyException(String.format("Key Type not supported: %s", jwk.getKeyType().getValue()));
}
return publicKey;
}
}

View File

@ -0,0 +1,91 @@
package io.spiffe.bundle.jwtbundle;
import io.spiffe.bundle.BundleSource;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.spiffeid.TrustDomain;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents a set of JWT bundles keyed by trust domain.
*/
@Value
public class JwtBundleSet implements BundleSource<JwtBundle> {
Map<TrustDomain, JwtBundle> bundles;
private JwtBundleSet(final Map<TrustDomain, JwtBundle> bundles) {
this.bundles = new ConcurrentHashMap<>(bundles);
}
private JwtBundleSet() {
this.bundles = new ConcurrentHashMap<>();
}
/**
* Creates a JWT bundle set from the list of JWT bundles.
*
* @param bundles Collection of {@link JwtBundle}
* @return a {@link JwtBundleSet}
*/
public static JwtBundleSet of(@NonNull final Collection<JwtBundle> bundles) {
if (bundles.size() == 0) {
throw new IllegalArgumentException("JwtBundle collection is empty");
}
final Map<TrustDomain, JwtBundle> bundleMap = new ConcurrentHashMap<>();
for (JwtBundle bundle : bundles) {
bundleMap.put(bundle.getTrustDomain(), bundle);
}
return new JwtBundleSet(bundleMap);
}
/**
* Creates a JWT bundle set empty.
*
* @return a {@link JwtBundleSet}
*/
public static JwtBundleSet emptySet() {
return new JwtBundleSet();
}
/**
* Gets the JWT bundle associated to a trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return a {@link JwtBundle} associated to the given trust domain
* @throws BundleNotFoundException if no bundle could be found for the given trust domain
*/
@Override
public JwtBundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
val bundle = bundles.get(trustDomain);
if (bundle == null) {
throw new BundleNotFoundException(String.format("No JWT bundle for trust domain %s", trustDomain));
}
return bundle;
}
/**
* Returns the map of JWT bundles keyed by trust domain.
*
* @return the map of JWT bundles keyed by trust domain
*/
public Map<TrustDomain, JwtBundle> getBundles() {
return Collections.unmodifiableMap(bundles);
}
/**
* Adds JWT bundle to this set, if the trust domain already exists
* replace the bundle.
*
* @param jwtBundle an instance of a JwtBundle.
*/
public void put(@NonNull final JwtBundle jwtBundle) {
bundles.put(jwtBundle.getTrustDomain(), jwtBundle);
}
}

View File

@ -0,0 +1,157 @@
package io.spiffe.bundle.x509bundle;
import io.spiffe.bundle.BundleSource;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.internal.CertificateUtils;
import io.spiffe.spiffeid.TrustDomain;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents a collection of trusted X.509 authorities for a trust domain.
*/
@Value
public class X509Bundle implements BundleSource<X509Bundle> {
TrustDomain trustDomain;
Set<X509Certificate> x509Authorities;
/**
* Creates a new X.509 bundle for a trust domain.
*
* @param trustDomain a {@link TrustDomain} to associate to the JwtBundle
*/
public X509Bundle(@NonNull final TrustDomain trustDomain) {
this.trustDomain = trustDomain;
this.x509Authorities = ConcurrentHashMap.newKeySet();
}
/**
* Creates a new X.509 bundle for a trust domain with X.509 Authorities.
*
* @param trustDomain a {@link TrustDomain} to associate to the JwtBundle
* @param x509Authorities a Map of X.509 Certificates
*/
public X509Bundle(@NonNull final TrustDomain trustDomain, @NonNull final Set<X509Certificate> x509Authorities) {
this.trustDomain = trustDomain;
this.x509Authorities = ConcurrentHashMap.newKeySet();
this.x509Authorities.addAll(x509Authorities);
}
/**
* Loads an X.509 bundle from a file on disk.
*
* @param trustDomain a {@link TrustDomain} to associate to the bundle
* @param bundlePath a path to the file that has the X.509 authorities
* @return an instance of {@link X509Bundle} with the X.509 authorities
* associated to the trust domain.
*
* @throws X509BundleException in case of failure accessing the given bundle path or the bundle cannot be parsed
*/
public static X509Bundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) throws X509BundleException {
final byte[] bundleBytes;
try {
bundleBytes = Files.readAllBytes(bundlePath);
} catch (IOException e) {
throw new X509BundleException("Unable to load X.509 bundle file", e);
}
val x509Certificates = generateX509Certificates(bundleBytes);
val x509CertificateSet = new HashSet<>(x509Certificates);
return new X509Bundle(trustDomain, x509CertificateSet);
}
/**
* Parses an X.509 bundle from an array of bytes.
*
* @param trustDomain a {@link TrustDomain} to associate to the X.509 bundle
* @param bundleBytes an array of bytes that represents the X.509 authorities
*
* @return an instance of {@link X509Bundle} with the X.509 authorities
* associated to the given trust domain
*
* @throws X509BundleException if the bundle cannot be parsed
*/
public static X509Bundle parse(@NonNull final TrustDomain trustDomain, @NonNull final byte[] bundleBytes) throws X509BundleException {
val x509Certificates = generateX509Certificates(bundleBytes);
val x509CertificateSet = new HashSet<>(x509Certificates);
return new X509Bundle(trustDomain, x509CertificateSet);
}
/**
* Returns the X.509 bundle associated to the trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link X509Bundle} associated to the given trust domain
*
* @throws BundleNotFoundException if no X.509 bundle can be found for the given trust domain
*/
@Override
public X509Bundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
if (this.trustDomain.equals(trustDomain)) {
return this;
}
throw new BundleNotFoundException(String.format("No X.509 bundle found for trust domain %s", trustDomain));
}
/**
* Returns the X.509 Authorities in the bundle.
*
* @return the X.509 Authorities in the bundle
*/
public Set<X509Certificate> getX509Authorities() {
return Collections.unmodifiableSet(x509Authorities);
}
/**
* Checks if the given X.509 authority exists in the bundle.
*
* @param x509Authority an X.509 certificate
* @return boolean true if the x509Authority is present in the X.509 bundle, false otherwise
*/
public boolean hasX509Authority(@NonNull final X509Certificate x509Authority) {
return x509Authorities.contains(x509Authority);
}
/**
* Adds an X.509 authority to the bundle.
*
* @param x509Authority an X.509 certificate
*/
public void addX509Authority(@NonNull final X509Certificate x509Authority) {
x509Authorities.add(x509Authority);
}
/**
* Removes an X.509 authority from the bundle.
*
* @param x509Authority an X.509 certificate
*/
public void removeX509Authority(@NonNull final X509Certificate x509Authority) {
x509Authorities.remove(x509Authority);
}
private static List<X509Certificate> generateX509Certificates(byte[] bundleBytes) throws X509BundleException {
List<X509Certificate> x509Certificates;
try {
x509Certificates = CertificateUtils.generateCertificates(bundleBytes);
} catch (CertificateParsingException e) {
throw new X509BundleException("Bundle certificates could not be parsed from bundle path", e);
}
return x509Certificates;
}
}

View File

@ -0,0 +1,92 @@
package io.spiffe.bundle.x509bundle;
import io.spiffe.bundle.BundleSource;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.spiffeid.TrustDomain;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents a set of X.509 bundles keyed by trust domain.
*/
@Value
public class X509BundleSet implements BundleSource<X509Bundle> {
Map<TrustDomain, X509Bundle> bundles;
private X509BundleSet(final Map<TrustDomain, X509Bundle> bundles) {
this.bundles = new ConcurrentHashMap<>(bundles);
}
private X509BundleSet() {
this.bundles = new ConcurrentHashMap<>();
}
/**
* Creates a new X.509 bundle set from a list of X.509 bundles.
*
* @param bundles Collection of {@link X509Bundle}
* @return a {@link X509BundleSet} initialized with the list of bundles
*/
public static X509BundleSet of(@NonNull final Collection<X509Bundle> bundles) {
if (bundles.size() == 0) {
throw new IllegalArgumentException("X509Bundles collection is empty");
}
final Map<TrustDomain, X509Bundle> bundleMap = new ConcurrentHashMap<>();
for (X509Bundle bundle : bundles) {
bundleMap.put(bundle.getTrustDomain(), bundle);
}
return new X509BundleSet(bundleMap);
}
/**
* Creates a new X.509 bundle empty.
*
* @return a {@link X509BundleSet}
*/
public static X509BundleSet emptySet() {
return new X509BundleSet();
}
/**
* Adds an X.509 bundle to this Set, if the trust domain already exists,
* replaces the bundle.
*
* @param x509Bundle a {@link X509Bundle}
*/
public void put(@NonNull final X509Bundle x509Bundle){
bundles.put(x509Bundle.getTrustDomain(), x509Bundle);
}
/**
* Returns the X.509 bundle associated to the trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link X509Bundle} associated to the given trust domain
* @throws BundleNotFoundException if no bundle could be found for the given trust domain
*/
@Override
public X509Bundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
val bundle = bundles.get(trustDomain);
if (bundle == null) {
throw new BundleNotFoundException(String.format("No X.509 bundle for trust domain %s", trustDomain));
}
return bundle;
}
/**
* Returns the X.509 bundles of this X.509 Bundle Set.
*
* @return the X.509 bundles of this X.509 Bundle Set
*/
public Map<TrustDomain, X509Bundle> getBundles() {
return Collections.unmodifiableMap(bundles);
}
}

View File

@ -0,0 +1,11 @@
package io.spiffe.exception;
/**
* Checked exception thrown to indicate that an Authority could not be
* found in the Bundle Source.
*/
public class AuthorityNotFoundException extends Exception {
public AuthorityNotFoundException(final String message) {
super(message);
}
}

View File

@ -0,0 +1,11 @@
package io.spiffe.exception;
/**
* Checked exception thrown to indicate that a Bundle could not be
* found in the Bundle Source.
*/
public class BundleNotFoundException extends Exception {
public BundleNotFoundException(final String message) {
super(message);
}
}

View File

@ -0,0 +1,11 @@
package io.spiffe.exception;
/**
* Runtime exception thrown when there is a validation error on
* a SpiffeId.
*/
public class InvalidSpiffeIdException extends RuntimeException {
public InvalidSpiffeIdException(String s) {
super(s);
}
}

View File

@ -0,0 +1,14 @@
package io.spiffe.exception;
/**
* Checked exception thrown when there is an error creating a JWT Bundle.
*/
public class JwtBundleException extends Exception {
public JwtBundleException(final String message) {
super(message);
}
public JwtBundleException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,15 @@
package io.spiffe.exception;
/**
* Checked thrown when there is an error creating or initializing a JWT Source.
*/
public class JwtSourceException extends Exception {
public JwtSourceException(final String message) {
super(message);
}
public JwtSourceException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,16 @@
package io.spiffe.exception;
/**
* Checked exception thrown when there is an error parsing
* the components of an JWT SVID.
*/
public class JwtSvidException extends Exception {
public JwtSvidException(final String message) {
super(message);
}
public JwtSvidException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,15 @@
package io.spiffe.exception;
/**
* Checked exception thrown to indicate that the socket endpoint address
* could not be parsed or is not valid.
*/
public class SocketEndpointAddressException extends Exception {
public SocketEndpointAddressException(final String message) {
super(message);
}
public SocketEndpointAddressException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,10 @@
package io.spiffe.exception;
/**
* Unchecked exception to be thrown by Watchers onError method.
*/
public class WatcherException extends RuntimeException {
public WatcherException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,15 @@
package io.spiffe.exception;
/**
* Checked exception thrown when there is an error parsing
* the components of an X.509 Bundle.
*/
public class X509BundleException extends Exception {
public X509BundleException(final String message) {
super(message);
}
public X509BundleException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,15 @@
package io.spiffe.exception;
/**
* Checked exception thrown when a there was an error retrieving
* or processing an X.509 Context.
*/
public class X509ContextException extends Exception {
public X509ContextException(final String message) {
super(message);
}
public X509ContextException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,14 @@
package io.spiffe.exception;
/**
* Checked thrown when there is an error creating or initializing an X.509 Source.
*/
public class X509SourceException extends Exception {
public X509SourceException(final String message) {
super(message);
}
public X509SourceException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,15 @@
package io.spiffe.exception;
/**
* Checked exception thrown when there is an error parsing
* the components of an X.509 SVID.
*/
public class X509SvidException extends Exception {
public X509SvidException(final String message) {
super(message);
}
public X509SvidException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,27 @@
package io.spiffe.internal;
public enum AsymmetricKeyAlgorithm {
RSA("RSA"),
EC("EC");
private final String value;
AsymmetricKeyAlgorithm(final String value) {
this.value = value;
}
public String value() {
return value;
}
public static AsymmetricKeyAlgorithm parse(String a) {
if ("RSA".equalsIgnoreCase(a)) {
return RSA;
} else if ("EC".equalsIgnoreCase(a)) {
return EC;
} else {
throw new IllegalArgumentException(String.format("Algorithm not supported: %s", a));
}
}
}

View File

@ -0,0 +1,241 @@
package io.spiffe.internal;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import lombok.val;
import java.io.ByteArrayInputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static io.spiffe.internal.AsymmetricKeyAlgorithm.EC;
import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA;
import static io.spiffe.internal.KeyUsage.CRL_SIGN;
import static io.spiffe.internal.KeyUsage.DIGITAL_SIGNATURE;
import static io.spiffe.internal.KeyUsage.KEY_CERT_SIGN;
import static org.apache.commons.lang3.StringUtils.startsWith;
/**
* Common certificate utility methods.
*/
public class CertificateUtils {
private static final String SPIFFE_PREFIX = "spiffe://";
private static final int SAN_VALUE_INDEX = 1;
private static final String PUBLIC_KEY_INFRASTRUCTURE_ALGORITHM = "PKIX";
private static final String X509_CERTIFICATE_TYPE = "X.509";
private CertificateUtils() {
}
/**
* Generates a list of X.509 certificates from a byte array.
*
* @param input as byte array representing a list of X.509 certificates, as a DER or PEM
* @return a List of {@link X509Certificate}
*/
public static List<X509Certificate> generateCertificates(final byte[] input) throws CertificateParsingException {
if (input.length == 0) {
throw new CertificateParsingException("No certificates found");
}
val certificateFactory = getCertificateFactory();
Collection<? extends Certificate> certificates;
try {
certificates = certificateFactory.generateCertificates(new ByteArrayInputStream(input));
} catch (CertificateException e) {
throw new CertificateParsingException("Certificate could not be parsed from cert bytes", e);
}
return certificates.stream()
.map(X509Certificate.class::cast)
.collect(Collectors.toList());
}
/**
* Generates a private key from an array of bytes.
*
* @param privateKeyBytes is a PEM or DER PKCS#8 private key.
* @return a instance of {@link PrivateKey}
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
public static PrivateKey generatePrivateKey(final byte[] privateKeyBytes, AsymmetricKeyAlgorithm algorithm, KeyFileFormat keyFileFormat) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException {
EncodedKeySpec kspec = getEncodedKeySpec(privateKeyBytes, keyFileFormat);
return generatePrivateKeyWithSpec(kspec, algorithm);
}
/**
* Validate a certificate chain with a set of trusted certificates.
*
* @param chain the certificate chain
* @param trustedCerts to validate the certificate chain
* @throws CertificateException
* @throws CertPathValidatorException
*/
public static void validate(final List<X509Certificate> chain, final Collection<X509Certificate> trustedCerts) throws CertificateException, CertPathValidatorException {
if (chain == null || chain.size() == 0) {
throw new IllegalArgumentException("Chain of certificates is empty");
}
val certificateFactory = getCertificateFactory();
PKIXParameters pkixParameters;
try {
pkixParameters = toPkixParameters(trustedCerts);
val certPath = certificateFactory.generateCertPath(chain);
getCertPathValidator().validate(certPath, pkixParameters);
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
throw new CertificateException(e);
}
}
/**
* Extracts the SPIFFE ID from an X.509 certificate.
* <p>
* It iterates over the list of SubjectAlternativesNames, read each entry, takes the value from the index
* defined in SAN_VALUE_INDEX and filters the entries that starts with the SPIFFE_PREFIX and returns the first.
*
* @param certificate a {@link X509Certificate}
* @return an instance of a {@link SpiffeId}
* @throws CertificateException if the certificate contains multiple SPIFFE IDs, or does not contain any, or
* the SAN extension cannot be decoded
*/
public static SpiffeId getSpiffeId(final X509Certificate certificate) throws CertificateException {
val spiffeIds = getSpiffeIds(certificate);
if (spiffeIds.size() > 1) {
throw new CertificateException("Certificate contains multiple SPIFFE IDs");
}
if (spiffeIds.size() < 1) {
throw new CertificateException("Certificate does not contain SPIFFE ID in the URI SAN");
}
return SpiffeId.parse(spiffeIds.get(0));
}
/**
* Extracts the trust domain of a chain of certificates.
*
* @param chain a list of {@link X509Certificate}
* @return a {@link TrustDomain}
* @throws CertificateException
*/
public static TrustDomain getTrustDomain(final List<X509Certificate> chain) throws CertificateException {
val spiffeId = getSpiffeId(chain.get(0));
return spiffeId.getTrustDomain();
}
public static boolean isCA(final X509Certificate cert) {
return cert.getBasicConstraints() != -1;
}
public static boolean hasKeyUsageCertSign(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
return keyUsage[KEY_CERT_SIGN.index()];
}
public static boolean hasKeyUsageDigitalSignature(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
return keyUsage[DIGITAL_SIGNATURE.index()];
}
public static boolean hasKeyUsageCRLSign(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
return keyUsage[CRL_SIGN.index()];
}
private static EncodedKeySpec getEncodedKeySpec(final byte[] privateKeyBytes, KeyFileFormat keyFileFormat) throws InvalidKeyException {
EncodedKeySpec keySpec;
if (keyFileFormat == KeyFileFormat.PEM) {
byte[] keyDer = toDerFormat(privateKeyBytes);
keySpec = new PKCS8EncodedKeySpec(keyDer);
} else {
keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
}
return keySpec;
}
private static List<String> getSpiffeIds(final X509Certificate certificate) throws CertificateParsingException {
if (certificate.getSubjectAlternativeNames() == null) {
return Collections.emptyList();
}
return certificate.getSubjectAlternativeNames()
.stream()
.map(san -> (String) san.get(SAN_VALUE_INDEX))
.filter(uri -> startsWith(uri, SPIFFE_PREFIX))
.collect(Collectors.toList());
}
private static PrivateKey generatePrivateKeyWithSpec(final EncodedKeySpec keySpec, AsymmetricKeyAlgorithm algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException {
PrivateKey privateKey = null;
switch (algorithm) {
case EC:
privateKey = KeyFactory.getInstance(EC.value()).generatePrivate(keySpec);
break;
case RSA:
privateKey = KeyFactory.getInstance(RSA.value()).generatePrivate(keySpec);
}
return privateKey;
}
// Create an instance of PKIXParameters used as input for the PKIX CertPathValidator
private static PKIXParameters toPkixParameters(final Collection<X509Certificate> trustedCerts) throws CertificateException, InvalidAlgorithmParameterException {
if (trustedCerts == null || trustedCerts.isEmpty()) {
throw new CertificateException("No trusted Certs");
}
val pkixParameters = new PKIXParameters(trustedCerts.stream()
.map(c -> new TrustAnchor(c, null))
.collect(Collectors.toSet()));
pkixParameters.setRevocationEnabled(false);
return pkixParameters;
}
// Get the default PKIX CertPath Validator
private static CertPathValidator getCertPathValidator() throws NoSuchAlgorithmException {
return CertPathValidator.getInstance(PUBLIC_KEY_INFRASTRUCTURE_ALGORITHM);
}
// Get the X.509 Certificate Factory
private static CertificateFactory getCertificateFactory() {
try {
return CertificateFactory.getInstance(X509_CERTIFICATE_TYPE);
} catch (CertificateException e) {
throw new IllegalStateException("Could not create Certificate Factory", e);
}
}
// Given a private key in PEM format, encode it as DER
private static byte[] toDerFormat(final byte[] privateKeyPem) throws InvalidKeyException {
String privateKeyAsString = new String(privateKeyPem);
privateKeyAsString = privateKeyAsString.replaceAll("(-+BEGIN PRIVATE KEY-+|-+END PRIVATE KEY-+)", "");
privateKeyAsString = privateKeyAsString.replaceAll("\r?\n", "");
val decoder = Base64.getDecoder();
try {
return decoder.decode(privateKeyAsString);
} catch (Exception e) {
throw new InvalidKeyException(e);
}
}
}

View File

@ -0,0 +1,131 @@
package io.spiffe.internal;
import lombok.NonNull;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Represents JWT Signature Supported Algorithms.
*/
public enum JwtSignatureAlgorithm {
/**
* ECDSA algorithm using SHA-256 hash algorithm.
*/
ES256("ES256"),
/**
* ECDSA algorithm using SHA-384 hash algorithm.
*/
ES384("ES384"),
/**
* ECDSA algorithm using SHA-512 hash algorithm.
*/
ES512("ES512"),
/**
* RSASSA-PKCS1-v1_5 algorithm using SHA-256 hash algorithm.
*/
RS256("RS256"),
/**
* RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm.
*/
RS384("RS384"),
/**
* RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm.
*/
RS512("RS512"),
/**
* RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256.
*/
PS256("PS256"),
/**
* RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384.
*/
PS384("PS384"),
/**
* RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512.
*/
PS512("PS512");
private final String name;
JwtSignatureAlgorithm(final String name) {
this.name = name;
}
public String getName() {
return name;
}
/**
* Represents families of algorithms.
*/
public enum Family {
RSA("RSA", RS256, RS384, RS512, PS256, PS384, PS512),
EC("EC", ES256, ES384, ES512);
private final String name;
private final Set<JwtSignatureAlgorithm> algorithms;
Family(final String name, final JwtSignatureAlgorithm... algs) {
this.name = name;
algorithms = new HashSet<>();
Collections.addAll(algorithms, algs);
}
public String getName() {
return name;
}
public boolean contains(final JwtSignatureAlgorithm a) {
return algorithms.contains(a);
}
public static Family parse(final String s) {
final Family family;
if (s.equals(RSA.getName())) {
family = RSA;
} else if (s.equals(EC.getName())) {
family = EC;
} else {
throw new IllegalArgumentException("Unsupported JWT family algorithm: " + s);
}
return family;
}
}
public static JwtSignatureAlgorithm parse(@NonNull final String s) {
final JwtSignatureAlgorithm algorithm;
if (s.equals(RS256.getName())) {
algorithm = RS256;
} else if (s.equals(RS384.getName())) {
algorithm = RS384;
} else if (s.equals(RS512.getName())) {
algorithm = RS512;
} else if (s.equals(ES256.getName())) {
algorithm = ES256;
} else if (s.equals(ES384.getName())) {
algorithm = ES384;
} else if (s.equals(ES512.getName())) {
algorithm = ES512;
} else if (s.equals(PS256.getName())) {
algorithm = PS256;
} else if (s.equals(PS384.getName())) {
algorithm = PS384;
} else if (s.equals(PS512.getName())) {
algorithm = PS512;
} else {
throw new IllegalArgumentException("Unsupported JWT algorithm: " + s);
}
return algorithm;
}
}

View File

@ -0,0 +1,6 @@
package io.spiffe.internal;
public enum KeyFileFormat {
PEM,
DER
}

View File

@ -0,0 +1,27 @@
package io.spiffe.internal;
/**
* Key Usages associated to their index in the X.509 key usage array.
*/
public enum KeyUsage {
DIGITAL_SIGNATURE(0),
NON_REPUDIATION(1),
KEY_ENCIPHERMENT(2),
DATA_ENCIPHERMENT(3),
KEY_AGREEMENT(4),
KEY_CERT_SIGN(5),
CRL_SIGN(6),
ENCIPHER_ONLY(7),
DECIPHER_ONLY(8);
private final int index;
public int index() {
return index;
}
KeyUsage(final int index) {
this.index = index;
}
}

View File

@ -0,0 +1,175 @@
package io.spiffe.spiffeid;
import io.spiffe.exception.InvalidSpiffeIdException;
import lombok.NonNull;
import lombok.Value;
import org.apache.commons.lang3.StringUtils;
import static io.spiffe.spiffeid.TrustDomain.isValidTrustDomainChar;
/**
* Represents a SPIFFE ID as defined in the SPIFFE standard.
* <p>
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md</a>
*/
@Value
public class SpiffeId {
static final String SPIFFE_SCHEME = "spiffe";
static final String SCHEME_PREFIX = SPIFFE_SCHEME + "://";
static final String EMPTY = "Cannot be empty";
static final String MISSING_TRUST_DOMAIN = "Trust domain is missing";
static final String WRONG_SCHEME = "Scheme is missing or invalid";
static final String BAD_TRUST_DOMAIN_CHAR = "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores";
static final String BAD_PATH_SEGMENT_CHAR = "Path segment characters are limited to letters, numbers, dots, dashes, and underscores";
static final String DOT_SEGMENT = "Path cannot contain dot segments";
static final String EMPTY_SEGMENT = "Path cannot contain empty segments";
static final String TRAILING_SLASH = "Path cannot have a trailing slash";
TrustDomain trustDomain;
String path;
private SpiffeId(final TrustDomain trustDomain, final String path) {
this.trustDomain = trustDomain;
this.path = path;
}
/**
* 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
*
* @param trustDomain an instance of a {@link TrustDomain}
* @param segments a list of string path segments
* @return a {@link SpiffeId}
* @throws InvalidSpiffeIdException if a given path segment contains an invalid char or empty or dot segment
*/
public static SpiffeId fromSegments(@NonNull final TrustDomain trustDomain, final String... segments) {
StringBuilder path = new StringBuilder();
for (String p : segments) {
validatePath(p);
path.append('/');
path.append(p);
}
return new SpiffeId(trustDomain, path.toString());
}
/**
* Parses a SPIFFE ID from a string (e.g. spiffe://example.org/test).
*
* @param id a String representing a SPIFFE ID
* @return A {@link SpiffeId}
* @throws IllegalArgumentException if the given string is empty
* @throws InvalidSpiffeIdException if the given string id contain an invalid scheme, invalid char or empty or dot segment
*/
public static SpiffeId parse(final String id) {
if (StringUtils.isBlank(id)) {
throw new IllegalArgumentException(EMPTY);
}
if (!id.contains(SCHEME_PREFIX)) {
throw new InvalidSpiffeIdException(WRONG_SCHEME);
}
String rest = id.substring(SCHEME_PREFIX.length());
int i = 0;
for (char c : rest.toCharArray()) {
if (c == '/'){
break;
}
if (!isValidTrustDomainChar(c)) {
throw new InvalidSpiffeIdException(BAD_TRUST_DOMAIN_CHAR);
}
i++;
}
if (i == 0) {
throw new InvalidSpiffeIdException(MISSING_TRUST_DOMAIN);
}
String td = rest.substring(0, i);
String path = rest.substring(i);
if (StringUtils.isNotBlank(path)) {
validatePath(path);
}
return new SpiffeId(new TrustDomain(td), path);
}
/**
* Returns true if the trust domain of this SPIFFE ID is the same as trust domain given as parameter.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return <code>true</code> if the given trust domain equals the trust domain of this object,
* <code>false</code> otherwise
*/
public boolean memberOf(final TrustDomain trustDomain) {
return this.trustDomain.equals(trustDomain);
}
/**
* Returns the string representation of the SPIFFE ID, concatenating schema, trust domain,
* and path segments (e.g. 'spiffe://example.org/path1/path2')
*/
@Override
public String toString() {
return String.format("%s://%s%s", SPIFFE_SCHEME, this.trustDomain.toString(), this.path);
}
/**
* 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
*/
public static void validatePath(String path) {
if (StringUtils.isBlank(path)) {
throw new IllegalArgumentException(EMPTY);
}
int segmentStart = 0;
int segmentEnd = 0;
for ( ; segmentEnd < path.length(); segmentEnd++) {
char c = path.charAt(segmentEnd);
if (c == '/') {
switch (path.substring(segmentStart, segmentEnd)) {
case "/":
throw new InvalidSpiffeIdException(EMPTY_SEGMENT);
case "/.":
case "/..":
throw new InvalidSpiffeIdException(DOT_SEGMENT);
}
segmentStart = segmentEnd;
continue;
}
if (!isValidPathSegmentChar(c)) {
throw new InvalidSpiffeIdException(BAD_PATH_SEGMENT_CHAR);
}
}
switch (path.substring(segmentStart, segmentEnd)) {
case "/":
throw new InvalidSpiffeIdException(TRAILING_SLASH);
case "/.":
case "/..":
throw new InvalidSpiffeIdException(DOT_SEGMENT);
}
}
private static boolean isValidPathSegmentChar(char c) {
if (c >= 'a' && c <= 'z')
return true;
if (c >= 'A' && c <= 'Z')
return true;
if (c >= '0' && c <= '9')
return true;
return c == '-' || c == '.' || c == '_';
}
}

View File

@ -0,0 +1,88 @@
package io.spiffe.spiffeid;
import com.google.common.collect.Sets;
import lombok.val;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* Utility class with methods to read SPIFFE IDs using different mechanisms.
*/
public final class SpiffeIdUtils {
private static final char DEFAULT_CHAR_SEPARATOR = '|';
private static final Set<Character> SUPPORTED_SEPARATORS = Sets.newHashSet(DEFAULT_CHAR_SEPARATOR, ' ', ',');
private SpiffeIdUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
/**
* Reads a file containing a list of SPIFFE IDs and parses them to {@link SpiffeId} instances.
* <p>
* The file should have one SPIFFE ID per line.
*
* @param spiffeIdsFile the path to the file containing a list of SPIFFE IDs
* @return a List of {@link SpiffeId} parsed from the file provided
* @throws IOException if the given spiffeIdsFile cannot be read
* @throws IllegalArgumentException if any of the SPIFFE IDs in the file cannot be parsed
*/
public static Set<SpiffeId> getSpiffeIdSetFromFile(final Path spiffeIdsFile) throws IOException {
try (val lines = Files.lines(spiffeIdsFile)) {
return lines
.map(SpiffeId::parse)
.collect(Collectors.toSet());
}
}
/**
* Parses a string representing a list of SPIFFE IDs and returns a list of instances of {@link SpiffeId}.
*
* @param spiffeIds a list of SPIFFE IDs represented in a string
* @param separator used to separate the SPIFFE IDs in the string. Only the pipe character and the blank space
* are valid separators
* @return a list of {@link SpiffeId} instances.
* @throws IllegalArgumentException is the separator provided is not valid
*/
public static Set<SpiffeId> toSetOfSpiffeIds(final String spiffeIds, final char separator) {
if (isBlank(spiffeIds)) {
return Collections.emptySet();
}
if (!SUPPORTED_SEPARATORS.contains(separator)) {
throw new IllegalArgumentException("Separator character is not supported.");
}
val array = parseArray(spiffeIds, separator);
return Arrays.stream(array)
.map(SpiffeId::parse)
.collect(Collectors.toSet());
}
/**
* Return the list of the SPIFFE IDs parsed from the String parameter, using the default separator (pipe character).
*
* @param spiffeIds a String representing a list of SPIFFE IDs separated by the pipe character
* @return a list of {@link SpiffeId} instances
* @throws IllegalArgumentException is the string provided is blank
*/
public static Set<SpiffeId> toSetOfSpiffeIds(final String spiffeIds) {
return toSetOfSpiffeIds(spiffeIds, DEFAULT_CHAR_SEPARATOR);
}
private static String[] parseArray(final String spiffeIds, final char separator) {
val regex = Pattern.quote(String.valueOf(separator));
return spiffeIds.trim().split(regex);
}
}

View File

@ -0,0 +1,99 @@
package io.spiffe.spiffeid;
import io.spiffe.exception.InvalidSpiffeIdException;
import lombok.NonNull;
import lombok.Value;
import org.apache.commons.lang3.StringUtils;
import static io.spiffe.spiffeid.SpiffeId.BAD_TRUST_DOMAIN_CHAR;
/**
* Represents the name of a SPIFFE trust domain (e.g. 'domain.test').
*/
@Value
public class TrustDomain {
String name;
TrustDomain(final String trustDomain) {
this.name = trustDomain;
}
/**
* Creates a trust domain.
*
* @param idOrName the name of a Trust Domain or a string representing a SpiffeId.
*
* @return an instance of a {@link TrustDomain}
* @throws IllegalArgumentException if the given string is empty.
* @throws InvalidSpiffeIdException if the given string contains an invalid char.
*/
public static TrustDomain parse(@NonNull final String idOrName) {
if (StringUtils.isBlank(idOrName)) {
throw new IllegalArgumentException("Trust domain is missing");
}
// Something looks kinda 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.
if (idOrName.contains(":/")) {
SpiffeId spiffeId = SpiffeId.parse(idOrName);
return spiffeId.getTrustDomain();
}
validateTrustDomainName(idOrName);
return new TrustDomain(idOrName);
}
/**
* Creates a SPIFFE ID from this trust domain and the given path segments.
*
* @param segments path segments
* @return a {@link SpiffeId} with the current trust domain and the given path segments
* @throws InvalidSpiffeIdException if the given path segments contain invalid chars or empty or dot segments
*/
public SpiffeId newSpiffeId(final String... segments) {
return SpiffeId.fromSegments(this, segments);
}
/**
* Returns the trust domain as a String.
*
* @return a String with the trust domain
*/
@Override
public String toString() {
return name;
}
/**
* Returns the trust domain as SPIFFE ID string (e.g. 'spiffe://example.org')
*
* @return a String formatted as a SPIFFE ID
*/
public String toIdString() {
return SpiffeId.SPIFFE_SCHEME + "://" + name;
}
static void validateTrustDomainName(final String name) {
for (char c : name.toCharArray()) {
if (!isValidTrustDomainChar(c)) {
throw new InvalidSpiffeIdException(BAD_TRUST_DOMAIN_CHAR);
}
}
}
static boolean isValidTrustDomainChar(char c) {
if (c >= 'a' && c <= 'z') {
return true;
}
if (c >= '0' && c <= '9') {
return true;
}
return c == '-' || c == '.' || c == '_';
}
}

View File

@ -0,0 +1,411 @@
package io.spiffe.svid.jwtsvid;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.AuthorityNotFoundException;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.InvalidSpiffeIdException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.internal.JwtSignatureAlgorithm;
import io.spiffe.spiffeid.SpiffeId;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents a SPIFFE JWT-SVID.
*/
@Value
public class JwtSvid {
/**
* SPIFFE ID of the JWT-SVID as present in the 'sub' claim.
*/
SpiffeId spiffeId;
/**
* Audience is the intended recipients of JWT-SVID as present in the 'aud' claim.
*/
Set<String> audience;
/**
* Expiration time of JWT-SVID as present in 'exp' claim.
*/
Date expiry;
/**
* Parsed claims from token.
*/
Map<String, Object> claims;
/**
* Serialized JWT token.
*/
String token;
/**
* Issued at time of JWT-SVID as present in 'iat' claim.
*/
Date issuedAt;
/**
* Hint is an operator-specified string used to provide guidance on how this
* identity should be used by a workload when more than one SVID is returned.
*/
String hint;
public static final String HEADER_TYP_JWT = "JWT";
public static final String HEADER_TYP_JOSE = "JOSE";
private JwtSvid(final SpiffeId spiffeId,
final Set<String> audience,
final Date issuedAt,
final Date expiry,
final Map<String, Object> claims,
final String token,
final String hint
) {
this.spiffeId = spiffeId;
this.audience = audience;
this.expiry = expiry;
this.claims = claims;
this.token = token;
this.issuedAt = issuedAt;
this.hint = hint;
}
/**
* Parses and validates a JWT-SVID token and returns an instance of {@link JwtSvid}.
* <p>
* The JWT-SVID signature is verified using the JWT bundle source.
*
* @param token a token as a string that is parsed and validated
* @param jwtBundleSource an implementation of a {@link BundleSource} that provides the JWT authorities to
* verify the signature
* @param audience audience as a list of strings used to validate the 'aud' claim
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing,
* when the algorithm is not supported (See {@link JwtSignatureAlgorithm}),
* when the header 'kid' is missing,
* when the header 'typ' is present and is not 'JWT' or 'JOSE'
* when the signature cannot be verified,
* when the 'aud' claim has an audience that is not in the audience list
* provided as parameter
* @throws IllegalArgumentException when the token is blank or cannot be parsed
* @throws BundleNotFoundException if the bundle for the trust domain of the spiffe id from the 'sub'
* cannot be found in the JwtBundleSource
* @throws AuthorityNotFoundException if the authority cannot be found in the bundle using the value from
* the 'kid' header
*/
public static JwtSvid parseAndValidate(@NonNull final String token,
@NonNull final BundleSource<JwtBundle> jwtBundleSource,
@NonNull final Set<String> audience)
throws JwtSvidException, BundleNotFoundException, AuthorityNotFoundException {
return parseAndValidate(token, jwtBundleSource, audience, null);
}
/**
* Parses and validates a JWT-SVID token and returns an instance of {@link JwtSvid}.
* <p>
* The JWT-SVID signature is verified using the JWT bundle source.
*
* @param token a token as a string that is parsed and validated
* @param jwtBundleSource an implementation of a {@link BundleSource} that provides the JWT authorities to
* verify the signature
* @param audience audience as a list of strings used to validate the 'aud' claim
* @param hint a hint that can be used to provide guidance on how this identity should be used
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing,
* when the algorithm is not supported (See {@link JwtSignatureAlgorithm}),
* when the header 'kid' is missing,
* when the header 'typ' is present and is not 'JWT' or 'JOSE'
* when the signature cannot be verified,
* when the 'aud' claim has an audience that is not in the audience list
* provided as parameter
* @throws IllegalArgumentException when the token is blank or cannot be parsed
* @throws BundleNotFoundException if the bundle for the trust domain of the spiffe id from the 'sub'
* cannot be found in the JwtBundleSource
* @throws AuthorityNotFoundException if the authority cannot be found in the bundle using the value from
* the 'kid' header
*/
public static JwtSvid parseAndValidate(@NonNull final String token,
@NonNull final BundleSource<JwtBundle> jwtBundleSource,
@NonNull final Set<String> audience,
final String hint
)
throws JwtSvidException, BundleNotFoundException, AuthorityNotFoundException {
if (StringUtils.isBlank(token)) {
throw new IllegalArgumentException("Token cannot be blank");
}
val signedJwt = getSignedJWT(token);
validateTypeHeader(signedJwt.getHeader());
JwtSignatureAlgorithm algorithm = parseAlgorithm(signedJwt.getHeader().getAlgorithm());
val claimsSet = getJwtClaimsSet(signedJwt);
validateAudience(claimsSet.getAudience(), audience);
val issuedAt = claimsSet.getIssueTime();
val expirationTime = claimsSet.getExpirationTime();
validateExpiration(expirationTime);
val spiffeId = getSpiffeIdOfSubject(claimsSet);
val jwtBundle = jwtBundleSource.getBundleForTrustDomain(spiffeId.getTrustDomain());
val keyId = getKeyId(signedJwt.getHeader());
val jwtAuthority = jwtBundle.findJwtAuthority(keyId);
verifySignature(signedJwt, jwtAuthority, algorithm, keyId);
val claimAudience = new HashSet<>(claimsSet.getAudience());
return new JwtSvid(spiffeId, claimAudience, issuedAt, expirationTime, claimsSet.getClaims(), token, hint);
}
/**
* Parses and validates a JWT-SVID token and returns an instance of a {@link JwtSvid}.
* <p>
* The JWT-SVID signature is not verified.
*
* @param token a token as a string that is parsed and validated
* @param audience audience as a list of strings used to validate the 'aud' claim
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing,
* when the 'aud' has an audience that is not in the audience provided as parameter,
* when the 'alg' is not supported (See {@link JwtSignatureAlgorithm}),
* when the header 'typ' is present and is not 'JWT' or 'JOSE'.
* @throws IllegalArgumentException when the token cannot be parsed
*/
public static JwtSvid parseInsecure(@NonNull final String token, @NonNull final Set<String> audience) throws JwtSvidException {
return parseInsecure(token, audience, null);
}
/**
* Parses and validates a JWT-SVID token and returns an instance of a {@link JwtSvid}.
* <p>
* The JWT-SVID signature is not verified.
*
* @param token a token as a string that is parsed and validated
* @param audience audience as a list of strings used to validate the 'aud'
* @param hint a hint that can be used to provide guidance on how this identity should be used
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing,
* when the 'aud' has an audience that is not in the audience provided as parameter,
* when the 'alg' is not supported (See {@link JwtSignatureAlgorithm}),
* when the header 'typ' is present and is not 'JWT' or 'JOSE'.
* @throws IllegalArgumentException when the token cannot be parsed
*/
public static JwtSvid parseInsecure(@NonNull final String token, @NonNull final Set<String> audience, final String hint) throws JwtSvidException {
if (StringUtils.isBlank(token)) {
throw new IllegalArgumentException("Token cannot be blank");
}
val signedJwt = getSignedJWT(token);
validateTypeHeader(signedJwt.getHeader());
parseAlgorithm(signedJwt.getHeader().getAlgorithm());
val claimsSet = getJwtClaimsSet(signedJwt);
validateAudience(claimsSet.getAudience(), audience);
val issuedAt = claimsSet.getIssueTime();
val expirationTime = claimsSet.getExpirationTime();
validateExpiration(expirationTime);
val spiffeId = getSpiffeIdOfSubject(claimsSet);
val claimAudience = new HashSet<>(claimsSet.getAudience());
return new JwtSvid(spiffeId, claimAudience, issuedAt, expirationTime, claimsSet.getClaims(), token, hint);
}
/**
* Returns the JWT-SVID marshaled to a string. The returned value is the same token value originally passed
* to the parseAndValidate method.
*
* @return the token as String
*/
public String marshal() {
return token;
}
/**
* Returns a copy of the expiration date time of the JWT SVID.
*
* @return a copy of the expiration date time of the JWT SVID
*/
public Date getExpiry() {
// defensive copy to prevent exposing a mutable object
return new Date(expiry.getTime());
}
/**
* Returns the SVID hint.
*
* @return the SVID hint
*/
public String getHint() {
return hint;
}
/**
* Returns the map of claims.
*
* @return the map of claims
*/
public Map<String, Object> getClaims() {
return Collections.unmodifiableMap(claims);
}
/**
* Returns the Set of audiences.
*
* @return the Set of audiences
*/
public Set<String> getAudience() {
return Collections.unmodifiableSet(audience);
}
private static JWTClaimsSet getJwtClaimsSet(final SignedJWT signedJwt) {
final JWTClaimsSet claimsSet;
try {
claimsSet = signedJwt.getJWTClaimsSet();
} catch (ParseException e) {
throw new IllegalArgumentException("Unable to parse JWT token", e);
}
return claimsSet;
}
private static SignedJWT getSignedJWT(final String token) {
final SignedJWT signedJwt;
try {
signedJwt = SignedJWT.parse(token);
} catch (ParseException e) {
throw new IllegalArgumentException("Unable to parse JWT token", e);
}
return signedJwt;
}
private static void verifySignature(final SignedJWT signedJwt, final PublicKey jwtAuthority, final JwtSignatureAlgorithm algorithm, final String keyId) throws JwtSvidException {
boolean verify;
try {
val verifier = getJwsVerifier(jwtAuthority, algorithm);
verify = signedJwt.verify(verifier);
} catch (ClassCastException | JOSEException e) {
throw new JwtSvidException(String.format("Error verifying signature with the authority with keyId=%s", keyId), e);
}
if (!verify) {
throw new JwtSvidException(String.format("Signature invalid: cannot be verified with the authority with keyId=%s", keyId));
}
}
private static JWSVerifier getJwsVerifier(final PublicKey jwtAuthority, final JwtSignatureAlgorithm algorithm) throws JOSEException, JwtSvidException {
JWSVerifier verifier;
if (JwtSignatureAlgorithm.Family.EC.contains(algorithm)) {
verifier = new ECDSAVerifier((ECPublicKey) jwtAuthority);
} else if (JwtSignatureAlgorithm.Family.RSA.contains(algorithm)) {
verifier = new RSASSAVerifier((RSAPublicKey) jwtAuthority);
} else {
throw new JwtSvidException(String.format("Unsupported token signature algorithm %s", algorithm));
}
return verifier;
}
private static String getKeyId(final JWSHeader header) throws JwtSvidException {
val keyId = header.getKeyID();
if (keyId == null) {
throw new JwtSvidException("Token header missing key id");
}
if (StringUtils.isBlank(keyId)) {
throw new JwtSvidException("Token header key id contains an empty value");
}
return keyId;
}
private static void validateExpiration(final Date expirationTime) throws JwtSvidException {
if (expirationTime == null) {
throw new JwtSvidException("Token missing expiration claim");
}
if (expirationTime.before(new Date())) {
throw new JwtSvidException("Token has expired");
}
}
private static SpiffeId getSpiffeIdOfSubject(final JWTClaimsSet claimsSet) throws JwtSvidException {
val subject = claimsSet.getSubject();
if (StringUtils.isBlank(subject)) {
throw new JwtSvidException("Token missing subject claim");
}
try {
return SpiffeId.parse(subject);
} catch (InvalidSpiffeIdException e) {
throw new JwtSvidException(String.format("Subject %s cannot be parsed as a SPIFFE ID", subject), e);
}
}
// expected audiences must be a subset of the audience claim in the token
private static void validateAudience(final List<String> audClaim, final Set<String> expectedAudiences) throws JwtSvidException {
if (!audClaim.containsAll(expectedAudiences)) {
throw new JwtSvidException(String.format("expected audience in %s (audience=%s)", expectedAudiences, audClaim));
}
}
private static JwtSignatureAlgorithm parseAlgorithm(JWSAlgorithm algorithm) throws JwtSvidException {
if (algorithm == null) {
throw new JwtSvidException("JWT header 'alg' is required");
}
try {
return JwtSignatureAlgorithm.parse(algorithm.getName());
} catch (IllegalArgumentException e) {
throw new JwtSvidException(e.getMessage(), e);
}
}
private static void validateTypeHeader(JWSHeader headers) throws JwtSvidException {
final JOSEObjectType type = headers.getType();
// if it's not present -> OK
if (type == null || StringUtils.isBlank(type.toString())) {
return;
}
final String typValue = type.toString();
if (!HEADER_TYP_JWT.equals(typValue) && !HEADER_TYP_JOSE.equals(typValue)) {
throw new JwtSvidException(String.format("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: '%s'.", type.toString()));
}
}
}

View File

@ -0,0 +1,55 @@
package io.spiffe.svid.jwtsvid;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.spiffeid.SpiffeId;
import lombok.NonNull;
import java.util.List;
/**
* Represents a source of SPIFFE JWT-SVIDs.
*/
public interface JwtSvidSource {
/**
* Fetches a JWT-SVID from the source with the given audiences.
*
* @param audience the audience
* @param extraAudiences a list of extra audiences as an array of String
* @return a {@link JwtSvid}
* @throws JwtSvidException when there is an error fetching the JWT SVID
*/
JwtSvid fetchJwtSvid(String audience, String... extraAudiences) throws JwtSvidException;
/**
* Fetches a JWT-SVID from the source with the given subject and audiences.
*
* @param subject a {@link SpiffeId}
* @param audience the audience
* @param extraAudiences a list of extra audiences as an array of String
* @return a {@link JwtSvid}
* @throws JwtSvidException when there is an error fetching the JWT SVID
*/
JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException;
/**
* Fetches all SPIFFE JWT-SVIDs on one-shot blocking call.
*
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return all of {@link JwtSvid} object
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
List<JwtSvid> fetchJwtSvids(@NonNull String audience, String... extraAudience) throws JwtSvidException;
/**
* Fetches all SPIFFE JWT-SVIDs on one-shot blocking call.
*
* @param subject a SPIFFE ID
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return all of {@link JwtSvid} object
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
List<JwtSvid> fetchJwtSvids(@NonNull SpiffeId subject, @NonNull String audience, String... extraAudience) throws JwtSvidException;
}

View File

@ -0,0 +1,283 @@
package io.spiffe.svid.x509svid;
import io.spiffe.exception.X509SvidException;
import io.spiffe.internal.AsymmetricKeyAlgorithm;
import io.spiffe.internal.CertificateUtils;
import io.spiffe.internal.KeyFileFormat;
import io.spiffe.spiffeid.SpiffeId;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.List;
/**
* Represents a SPIFFE X.509 SVID.
* <p>
* Contains a SPIFFE ID, a private key and a chain of X.509 certificates.
*/
@Value
public class X509Svid {
SpiffeId spiffeId;
/**
* The X.509 certificates of the X.509-SVID. The leaf certificate is
* the X.509-SVID certificate. Any remaining certificates (if any) chain
* the X.509-SVID certificate back to an X.509 root for the trust domain.
*/
List<X509Certificate> chain;
PrivateKey privateKey;
/**
* Hint is an operator-specified string used to provide guidance on how this
* identity should be used by a workload when more than one SVID is returned.
*/
String hint;
private X509Svid(
final SpiffeId spiffeId,
final List<X509Certificate> chain,
final PrivateKey privateKey,
final String hint
) {
this.spiffeId = spiffeId;
this.chain = chain;
this.privateKey = privateKey;
this.hint = hint;
}
/**
* Returns the Leaf X.509 certificate of the chain.
*
* @return the Leaf X.509 certificate of the chain
*/
public X509Certificate getLeaf() {
return chain.get(0);
}
/**
* Returns the SVID hint.
*
* @return the SVID hint
*/
public String getHint() {
return hint;
}
/**
* Returns the chain of X.509 certificates.
*
* @return the chain of X.509 certificates
*/
public List<X509Certificate> getChain() {
return Collections.unmodifiableList(chain);
}
/**
* Loads the X.509 SVID from PEM encoded files on disk.
* <p>
* It is assumed that the leaf certificate is always the first certificate in the parsed chain.
*
* @param certsFilePath path to X.509 certificate chain file
* @param privateKeyFilePath path to private key file
* @return an instance of {@link X509Svid}
* @throws X509SvidException if there is an error parsing the given certsFilePath or the privateKeyFilePath
*/
public static X509Svid load(@NonNull final Path certsFilePath, @NonNull final Path privateKeyFilePath)
throws X509SvidException {
final byte[] certsBytes;
try {
certsBytes = Files.readAllBytes(certsFilePath);
} catch (IOException e) {
throw new X509SvidException("Cannot read certificate file", e);
}
final byte[] privateKeyBytes;
try {
privateKeyBytes = Files.readAllBytes(privateKeyFilePath);
} catch (IOException e) {
throw new X509SvidException("Cannot read private key file", e);
}
return createX509Svid(certsBytes, privateKeyBytes, KeyFileFormat.PEM, null);
}
/**
* Parses the X.509 SVID from PEM or DER blocks containing certificate chain and key
* bytes. The key must be a PEM block with PKCS#8.
* <p>
* It is assumed that the leaf certificate is always the first certificate in the parsed chain.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key as byte array
* @return a {@link X509Svid} parsed from the given certBytes and privateKeyBytes
* @throws X509SvidException if the given certsBytes or privateKeyBytes cannot be parsed
*/
public static X509Svid parse(@NonNull final byte[] certsBytes, @NonNull final byte[] privateKeyBytes)
throws X509SvidException {
return parse(certsBytes, privateKeyBytes, null);
}
/**
* Parses the X.509 SVID from PEM or DER blocks containing certificate chain and key
* bytes. The key must be a PEM block with PKCS#8.
* <p>
* It is assumed that the leaf certificate is always the first certificate in the parsed chain.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key as byte array
* @param hint a hint that can be used to provide guidance on how this identity should be used
* @return a {@link X509Svid} parsed from the given certBytes and privateKeyBytes
* @throws X509SvidException if the given certsBytes or privateKeyBytes cannot be parsed
*/
public static X509Svid parse(@NonNull final byte[] certsBytes, @NonNull final byte[] privateKeyBytes, final String hint)
throws X509SvidException {
return createX509Svid(certsBytes, privateKeyBytes, KeyFileFormat.PEM, hint);
}
/**
* Parses the X509-SVID from certificate and key bytes. The certificate must be ASN.1 DER (concatenated with
* no intermediate padding if there are more than one certificate). The key must be a PKCS#8 ASN.1 DER.
* <p>
* It is assumed that the leaf certificate is always the first certificate in the parsed chain.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key as byte array
* @return a {@link X509Svid} parsed from the given certBytes and privateKeyBytes
* @throws X509SvidException if the given certsBytes or privateKeyBytes cannot be parsed
*/
public static X509Svid parseRaw(@NonNull final byte[] certsBytes,
@NonNull final byte[] privateKeyBytes) throws X509SvidException {
return parseRaw(certsBytes, privateKeyBytes, null);
}
/**
* Parses the X509-SVID from certificate and key bytes. The certificate must be ASN.1 DER (concatenated with
* no intermediate padding if there are more than one certificate). The key must be a PKCS#8 ASN.1 DER.
* <p>
* It is assumed that the leaf certificate is always the first certificate in the parsed chain.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key as byte array
* @param hint a hint that can be used to provide guidance on how this identity should be used
* @return a {@link X509Svid} parsed from the given certBytes and privateKeyBytes
* @throws X509SvidException if the given certsBytes or privateKeyBytes cannot be parsed
*/
public static X509Svid parseRaw(@NonNull final byte[] certsBytes,
@NonNull final byte[] privateKeyBytes,
final String hint) throws X509SvidException {
return createX509Svid(certsBytes, privateKeyBytes, KeyFileFormat.DER, hint);
}
/**
* Returns the chain of certificates as an array of {@link X509Certificate}.
*
* @return the chain of certificates as an array of {@link X509Certificate}
*/
public X509Certificate[] getChainArray() {
return chain.toArray(new X509Certificate[0]);
}
private static X509Svid createX509Svid(final byte[] certsBytes,
final byte[] privateKeyBytes,
final KeyFileFormat keyFileFormat,
final String hint) throws X509SvidException {
val x509Certificates = generateX509Certificates(certsBytes);
val privateKey = generatePrivateKey(privateKeyBytes, keyFileFormat, x509Certificates);
val spiffeId = getSpiffeId(x509Certificates);
validateLeafCertificate(x509Certificates.get(0));
// there are intermediate CA certificates
if (x509Certificates.size() > 1) {
validateSigningCertificates(x509Certificates);
}
return new X509Svid(spiffeId, x509Certificates, privateKey, hint);
}
private static SpiffeId getSpiffeId(final List<X509Certificate> x509Certificates) throws X509SvidException {
final SpiffeId spiffeId;
try {
spiffeId = CertificateUtils.getSpiffeId(x509Certificates.get(0));
} catch (CertificateException e) {
throw new X509SvidException(e.getMessage(), e);
}
return spiffeId;
}
private static PrivateKey generatePrivateKey(final byte[] privateKeyBytes,
final KeyFileFormat keyFileFormat,
final List<X509Certificate> x509Certificates)
throws X509SvidException {
val publicKeyCertAlgorithm = x509Certificates.get(0).getPublicKey().getAlgorithm();
val algorithm = AsymmetricKeyAlgorithm.parse(publicKeyCertAlgorithm);
final PrivateKey privateKey;
try {
privateKey = CertificateUtils.generatePrivateKey(privateKeyBytes, algorithm, keyFileFormat);
} catch (InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new X509SvidException("Private Key could not be parsed from key bytes", e);
}
return privateKey;
}
private static List<X509Certificate> generateX509Certificates(final byte[] certsBytes) throws X509SvidException {
final List<X509Certificate> x509Certificates;
try {
x509Certificates = CertificateUtils.generateCertificates(certsBytes);
} catch (CertificateParsingException e) {
throw new X509SvidException("Certificate could not be parsed from cert bytes", e);
}
return x509Certificates;
}
private static void validateSigningCertificates(final List<X509Certificate> certificates) throws X509SvidException {
for (int i = 1; i < certificates.size(); i++) {
verifyCaCert(certificates.get(i));
}
}
private static void verifyCaCert(final X509Certificate cert) throws X509SvidException {
if (!CertificateUtils.isCA(cert)) {
throw new X509SvidException("Signing certificate must have CA flag set to true");
}
if (!CertificateUtils.hasKeyUsageCertSign(cert)) {
throw new X509SvidException("Signing certificate must have 'keyCertSign' as key usage");
}
}
private static void validateLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (CertificateUtils.isCA(leaf)) {
throw new X509SvidException("Leaf certificate must not have CA flag set to true");
}
validateKeyUsageOfLeafCertificate(leaf);
}
private static void validateKeyUsageOfLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (!CertificateUtils.hasKeyUsageDigitalSignature(leaf)) {
throw new X509SvidException("Leaf certificate must have 'digitalSignature' as key usage");
}
if (CertificateUtils.hasKeyUsageCertSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'keyCertSign' as key usage");
}
if (CertificateUtils.hasKeyUsageCRLSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'cRLSign' as key usage");
}
}
}

View File

@ -0,0 +1,14 @@
package io.spiffe.svid.x509svid;
/**
* Represents a source of X.509 SVIDs.
*/
public interface X509SvidSource {
/**
* Returns the X.509 SVID in the source.
*
* @return an instance of a {@link X509Svid}
*/
X509Svid getX509Svid();
}

View File

@ -0,0 +1,79 @@
package io.spiffe.svid.x509svid;
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.internal.CertificateUtils;
import io.spiffe.spiffeid.SpiffeId;
import lombok.NonNull;
import lombok.extern.java.Log;
import lombok.val;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
/**
* Provides methods to validate a chain of X.509 certificates using an X.509 bundle source.
*/
@Log
public final class X509SvidValidator {
private X509SvidValidator() {
}
/**
* Verifies that a chain of certificates can be chained to one authority in the given X.509 bundle source.
*
* @param chain a list representing the chain of X.509 certificates to be validated
* @param x509BundleSource a {@link BundleSource } to provide the authorities
* @throws CertificateException is the chain cannot be verified with an authority from the X.509 bundle source
* @throws BundleNotFoundException if no X.509 bundle for the trust domain could be found in the X.509 bundle source
* @throws NullPointerException if the given chain or 509BundleSource are null
*/
public static void verifyChain(
@NonNull final List<X509Certificate> chain,
@NonNull final BundleSource<X509Bundle> x509BundleSource)
throws CertificateException, BundleNotFoundException {
val trustDomain = CertificateUtils.getTrustDomain(chain);
val x509Bundle = x509BundleSource.getBundleForTrustDomain(trustDomain);
try {
CertificateUtils.validate(chain, x509Bundle.getX509Authorities());
} catch (CertPathValidatorException e) {
throw new CertificateException("Cert chain cannot be verified", e);
}
}
/**
* Checks that the X.509 SVID provided has a SPIFFE ID that is in the Set of accepted SPIFFE IDs supplied.
*
* @param x509Certificate a {@link X509Svid} with a SPIFFE ID to be verified
* @param acceptedSpiffeIdsSupplier a {@link Supplier} of a Set of SPIFFE IDs that are accepted
* @throws CertificateException if the SPIFFE ID in x509Certificate is not in the Set supplied by
* acceptedSpiffeIdsSupplier, or if the SPIFFE ID cannot be parsed from the
* x509Certificate
* @throws NullPointerException if the given x509Certificate or acceptedSpiffeIdsSupplier are null
*/
public static void verifySpiffeId(@NonNull final X509Certificate x509Certificate,
@NonNull final Supplier<Set<SpiffeId>> acceptedSpiffeIdsSupplier)
throws CertificateException {
val spiffeIdSet = acceptedSpiffeIdsSupplier.get();
if (spiffeIdSet.isEmpty()) {
String error = "The supplier of accepted SPIFFE IDs supplied an empty set";
log.warning(error);
throw new CertificateException(error);
}
val spiffeId = CertificateUtils.getSpiffeId(x509Certificate);
if (!spiffeIdSet.contains(spiffeId)) {
val error = String.format("SPIFFE ID %s in X.509 certificate is not accepted", spiffeId);
log.warning(String.format("Client SPIFFE ID validation failed: %s", error));
throw new CertificateException(String.format(error, spiffeId));
}
}
}

View File

@ -0,0 +1,176 @@
package io.spiffe.workloadapi;
import io.spiffe.exception.SocketEndpointAddressException;
import lombok.NonNull;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.InetAddressValidator;
import java.net.URI;
import java.net.URISyntaxException;
import static io.spiffe.workloadapi.AddressScheme.UNIX_SCHEME;
/**
* Parses and validates Workload API socket addresses following the SPIFFE standard and provides
* the default Workload API address.
* <p>
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint">SPIFFE Workload Endpoint Standard</a>
*/
public class Address {
/**
* Environment variable holding the default Workload API address.
*/
public static final String SOCKET_ENV_VARIABLE = "SPIFFE_ENDPOINT_SOCKET";
private Address() {
}
/**
* Returns the default Workload API address hold by the system environment variable.
*
* @return the default Workload API address hold by the system environment variable
* defined by SOCKET_ENV_VARIABLE.
* @throws IllegalStateException is the Environment variable is not set
*/
public static String getDefaultAddress() {
String address = System.getenv(Address.SOCKET_ENV_VARIABLE);
if (StringUtils.isBlank(address)) {
String error = String.format("Endpoint Socket Address Environment Variable is not set: %s", SOCKET_ENV_VARIABLE);
throw new IllegalStateException(error);
}
return address;
}
/**
* Parses and validates a Workload API socket address.
* <p>
* The value of the address is structured as an RFC 3986 URI. The scheme MUST be set to either unix or tcp,
* which indicates that the endpoint is served over a Unix Domain Socket or a TCP listen socket, respectively.
* <p>
* If the scheme is set to unix, then the authority component MUST NOT be set, and the path component MUST be set
* to the absolute path of the SPIFFE Workload Endpoint Unix Domain Socket (e.g. unix:///path/to/endpoint.sock).
* The scheme and path components are mandatory, and no other component may be set.
* <p>
* If the scheme is set to tcp, then the host component of the authority MUST be set to an IP address,
* and the port component of the authority MUST be set to the TCP port number of the
* SPIFFE Workload Endpoint TCP listen socket.
* The scheme, host, and port components are mandatory, and no other component may be set.
* As an example, tcp://127.0.0.1:8000 is valid, and tcp://127.0.0.1:8000/foo is not.
*
* @param address the Workload API socket address as a string
* @return an instance of a {@link URI}
* @throws SocketEndpointAddressException if the address could not be parsed or if it doesn't complain to the rules
* defined in the SPIFFE Worload Endpoint Standard.
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint">SPIFFE Workload Endpoint Standard</a>
*/
public static URI parseAddress(@NonNull final String address) throws SocketEndpointAddressException {
val parsedAddress = parseUri(address);
val scheme = getScheme(parsedAddress);
if (scheme == UNIX_SCHEME) {
validateUnixAddress(parsedAddress);
} else {
validateTcpAddress(parsedAddress);
}
return parsedAddress;
}
private static URI parseUri(final String address) throws SocketEndpointAddressException {
final URI parsedAddress;
try {
parsedAddress = new URI(address);
} catch (URISyntaxException e) {
val error = "Workload endpoint socket is not a valid URI: %s";
throw new SocketEndpointAddressException(String.format(error, address), e);
}
return parsedAddress;
}
private static AddressScheme getScheme(final URI parsedAddress) throws SocketEndpointAddressException {
try {
val scheme = parsedAddress.getScheme();
return AddressScheme.parseScheme(scheme);
} catch (IllegalArgumentException e) {
val error = "Workload endpoint socket URI must have a tcp:// or unix:// scheme: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress.toString()));
}
}
private static void validateUnixAddress(final URI parsedAddress) throws SocketEndpointAddressException {
if (parsedAddress.isOpaque()) {
val error = "Workload endpoint unix socket URI must not be opaque: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getRawAuthority())) {
val error = "Workload endpoint unix socket URI must not include authority component: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (hasEmptyPath(parsedAddress.getPath())) {
val error = "Workload endpoint unix socket path cannot be blank: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getRawQuery())) {
val error = "Workload endpoint unix socket URI must not include query values: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getFragment())) {
val error = "Workload endpoint unix socket URI must not include a fragment: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
}
private static void validateTcpAddress(final URI parsedAddress) throws SocketEndpointAddressException {
if (parsedAddress.isOpaque()) {
val error = "Workload endpoint tcp socket URI must not be opaque: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getUserInfo())) {
val error = "Workload endpoint tcp socket URI must not include user info: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isBlank(parsedAddress.getHost())) {
final String error = "Workload endpoint tcp socket URI must include a host: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getPath())) {
val error = "Workload endpoint tcp socket URI must not include a path: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getRawQuery())) {
val error = "Workload endpoint tcp socket URI must not include query values: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
if (StringUtils.isNotBlank(parsedAddress.getFragment())) {
val error = "Workload endpoint tcp socket URI must not include a fragment: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
val ipValid = InetAddressValidator.getInstance().isValid(parsedAddress.getHost());
if (!ipValid) {
val error = "Workload endpoint tcp socket URI host component must be an IP:port: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
int port = parsedAddress.getPort();
if (port == -1) {
final String error = "Workload endpoint tcp socket URI host component must include a port: %s";
throw new SocketEndpointAddressException(String.format(error, parsedAddress));
}
}
private static boolean hasEmptyPath(final String path) {
return StringUtils.isBlank(path) || "/".equals(path);
}
}

View File

@ -0,0 +1,32 @@
package io.spiffe.workloadapi;
/**
* Address Scheme names enum.
*/
public enum AddressScheme {
UNIX_SCHEME("unix"),
TCP_SCHEME("tcp");
private final String name;
AddressScheme(final String scheme) {
this.name = scheme;
}
/**
* Parses and returns an AddressScheme instance.
*
* @param scheme a string representing an Address Scheme ('unix' or 'tcp')
* @return the enum instance representing the scheme
* @throws IllegalArgumentException if the scheme is not 'unix' or 'tcp'
*/
public static AddressScheme parseScheme(String scheme) {
if ("unix".equals(scheme)) {
return UNIX_SCHEME;
} else if ("tcp".equals(scheme)) {
return TCP_SCHEME;
} else {
throw new IllegalArgumentException("Address Scheme not supported: ");
}
}
}

View File

@ -0,0 +1,338 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.*;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.jwtsvid.JwtSvid;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import lombok.val;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.io.Closeable;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import static io.spiffe.workloadapi.internal.ThreadUtils.await;
/**
* Represents a source of SPIFFE JWT SVIDs and JWT bundles maintained via the Workload API.
* The JWT SVIDs are cached and fetchJwtSvid methods return from cache
* checking that the JWT SVID has still at least half of its lifetime.
*/
@Log
public class CachedJwtSource implements JwtSource {
static final String TIMEOUT_SYSTEM_PROPERTY = "spiffe.newJwtSource.timeout";
static final Duration DEFAULT_TIMEOUT =
Duration.parse(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, "PT0S"));
// Synchronized map of JWT SVIDs, keyed by a pair of SPIFFE ID and a Set of audiences strings.
// This map is used to cache the JWT SVIDs and avoid fetching them from the Workload API.
private final
Map<ImmutablePair<SpiffeId, Set<String>>, List<JwtSvid>> jwtSvids = new ConcurrentHashMap<>();
private JwtBundleSet bundles;
private final WorkloadApiClient workloadApiClient;
private volatile boolean closed;
private Clock clock;
// private constructor
private CachedJwtSource(final WorkloadApiClient workloadApiClient) {
this.clock = Clock.systemDefaultZone();
this.workloadApiClient = workloadApiClient;
}
/**
* Creates a new Cached JWT source. It blocks until the initial update with the JWT bundles
* has been received from the Workload API or until the timeout configured
* through the system property `spiffe.newJwtSource.timeout` expires.
* If no timeout is configured, it blocks until it gets a JWT update from the Workload API.
* <p>
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
*
* @return an instance of {@link DefaultJwtSource}, with the JWT bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws JwtSourceException if the source could not be initialized
*/
public static JwtSource newSource() throws JwtSourceException, SocketEndpointAddressException {
JwtSourceOptions options = JwtSourceOptions.builder().initTimeout(DEFAULT_TIMEOUT).build();
return newSource(options);
}
/**
* Creates a new JWT source. It blocks until the initial update with the JWT bundles
* has been received from the Workload API, doing retries with an exponential backoff policy,
* or until the initTimeout has expired.
* <p>
* If the timeout is not provided in the options, the default timeout is read from the
* system property `spiffe.newJwtSource.timeout`. If none is configured, this method will
* block until the JWT bundles can be retrieved from the Workload API.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
*
* @param options {@link JwtSourceOptions}
* @return an instance of {@link CachedJwtSource}, with the JWT bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws JwtSourceException if the source could not be initialized
*/
public static JwtSource newSource(@NonNull final JwtSourceOptions options)
throws SocketEndpointAddressException, JwtSourceException {
if (options.getWorkloadApiClient() == null) {
options.setWorkloadApiClient(createClient(options));
}
if (options.getInitTimeout() == null) {
options.setInitTimeout(DEFAULT_TIMEOUT);
}
CachedJwtSource jwtSource = new CachedJwtSource(options.getWorkloadApiClient());
try {
jwtSource.init(options.getInitTimeout());
} catch (Exception e) {
jwtSource.close();
throw new JwtSourceException("Error creating JWT source", e);
}
return jwtSource;
}
/**
* Fetches a JWT SVID for the given audiences. The JWT SVID is cached and
* returned from the cache if it still has at least half of its lifetime.
*
* @param audience the audience
* @param extraAudiences a list of extra audiences as an array of String
* @return a {@link JwtSvid}
* @throws JwtSvidException
*/
@Override
public JwtSvid fetchJwtSvid(final String audience, final String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return getJwtSvids(audience, extraAudiences).get(0);
}
/**
* Fetches a JWT SVID for the given subject and audience. The JWT SVID is cached and
* returned from cache if it has still at least half of its lifetime.
*
* @return a {@link JwtSvid}
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtSvid fetchJwtSvid(final SpiffeId subject, final String audience, final String... extraAudiences)
throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return getJwtSvids(subject, audience, extraAudiences).get(0);
}
/**
* Fetches a list of JWT SVIDs for the given audience. The JWT SVIDs are cached and
* returned from cache if they have still at least half of their lifetime.
*
* @return a list of {@link JwtSvid}s
* @throws IllegalStateException if the source is closed
*/
@Override
public List<JwtSvid> fetchJwtSvids(final String audience, final String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return getJwtSvids(audience, extraAudiences);
}
/**
* Fetches a list of JWT SVIDs for the given subject and audience. The JWT SVIDs are cached and
* returned from cache if they have still at least half of their lifetime.
*
* @return a list of {@link JwtSvid}s
* @throws IllegalStateException if the source is closed
*/
@Override
public List<JwtSvid> fetchJwtSvids(final SpiffeId subject, final String audience, final String... extraAudiences)
throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return getJwtSvids(subject, audience, extraAudiences);
}
/**
* Returns the JWT bundle for a given trust domain.
*
* @return an instance of a {@link X509Bundle}
* @throws BundleNotFoundException is there is no bundle for the trust domain provided
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtBundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("JWT bundle source is closed");
}
return bundles.getBundleForTrustDomain(trustDomain);
}
/**
* Closes this source, dropping the connection to the Workload API.
* Other source methods will return an error after close has been called.
* <p>
* It is marked with {@link SneakyThrows} because it is not expected to throw
* the checked exception defined on the {@link Closeable} interface.
*/
@SneakyThrows
@Override
public void close() {
if (!closed) {
synchronized (this) {
if (!closed) {
workloadApiClient.close();
closed = true;
}
}
}
}
// Check if the jwtSvids map contains the cacheKey, returns it if it does and the JWT SVID has not passed its half lifetime.
// If the cache does not contain the key or the JWT SVID has passed its half lifetime, make a new FetchJWTSVID call to the Workload API,
// adds the JWT SVIDs to the cache map and returns them.
// Only one thread can fetch new JWT SVIDs and update the cache at a time.
private List<JwtSvid> getJwtSvids(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException {
Set<String> audiencesSet = getAudienceSet(audience, extraAudiences);
ImmutablePair<SpiffeId, Set<String>> cacheKey = new ImmutablePair<>(subject, audiencesSet);
List<JwtSvid> svidList = jwtSvids.get(cacheKey);
if (svidList != null && !isTokenPastHalfLifetime(svidList.get(0))) {
return svidList;
}
// even using ConcurrentHashMap, there is a possibility of multiple threads trying to fetch new JWT SVIDs at the same time.
synchronized (this) {
// Check again if the jwtSvids map contains the cacheKey, and return the entry if it exists and the JWT SVID has not passed its half lifetime.
// If it does not exist or the JWT-SVID has passed half its lifetime, call the Workload API to fetch new JWT-SVIDs,
// add them to the cache map, and return the list of JWT-SVIDs.
svidList = jwtSvids.get(cacheKey);
if (svidList != null && !isTokenPastHalfLifetime(svidList.get(0))) {
return svidList;
}
if (cacheKey.left == null) {
svidList = workloadApiClient.fetchJwtSvids(audience, extraAudiences);
} else {
svidList = workloadApiClient.fetchJwtSvids(cacheKey.left, audience, extraAudiences);
}
jwtSvids.put(cacheKey, svidList);
return svidList;
}
}
private List<JwtSvid> getJwtSvids(String audience, String... extraAudiences) throws JwtSvidException {
return getJwtSvids(null, audience, extraAudiences);
}
private static Set<String> getAudienceSet(String audience, String[] extraAudiences) {
Set<String> audiencesString;
if (extraAudiences != null && extraAudiences.length > 0) {
audiencesString = new HashSet<>(Arrays.asList(extraAudiences));
audiencesString.add(audience);
} else {
audiencesString = Collections.singleton(audience);
}
return audiencesString;
}
private boolean isTokenPastHalfLifetime(JwtSvid jwtSvid) {
Instant now = clock.instant();
val halfLife = new Date(jwtSvid.getExpiry().getTime() - (jwtSvid.getExpiry().getTime() - jwtSvid.getIssuedAt().getTime()) / 2);
val halfLifeInstant = Instant.ofEpochMilli(halfLife.getTime());
return now.isAfter(halfLifeInstant);
}
private void init(final Duration timeout) throws TimeoutException {
CountDownLatch done = new CountDownLatch(1);
setJwtBundlesWatcher(done);
boolean success;
if (timeout.isZero()) {
await(done);
success = true;
} else {
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
}
if (!success) {
throw new TimeoutException("Timeout waiting for JWT bundles update");
}
}
private void setJwtBundlesWatcher(final CountDownLatch done) {
workloadApiClient.watchJwtBundles(new Watcher<JwtBundleSet>() {
@Override
public void onUpdate(final JwtBundleSet update) {
log.log(Level.INFO, "Received JwtBundleSet update");
setJwtBundleSet(update);
done.countDown();
}
@Override
public void onError(final Throwable error) {
log.log(Level.SEVERE, "Error in JwtBundleSet watcher", error);
done.countDown();
throw new WatcherException("Error fetching JwtBundleSet", error);
}
});
}
private void setJwtBundleSet(final JwtBundleSet update) {
synchronized (this) {
this.bundles = update;
}
}
private boolean isClosed() {
synchronized (this) {
return closed;
}
}
private static WorkloadApiClient createClient(final JwtSourceOptions options)
throws SocketEndpointAddressException {
val clientOptions = DefaultWorkloadApiClient.ClientOptions
.builder()
.spiffeSocketPath(options.getSpiffeSocketPath())
.build();
return DefaultWorkloadApiClient.newClient(clientOptions);
}
void setClock(Clock clock) {
this.clock = clock;
}
}

View File

@ -0,0 +1,240 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.*;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.jwtsvid.JwtSvid;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import lombok.val;
import java.io.Closeable;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import static io.spiffe.workloadapi.internal.ThreadUtils.await;
/**
* Represents a source of SPIFFE JWT SVIDs and JWT bundles maintained via the Workload API.
*/
@Log
public class DefaultJwtSource implements JwtSource {
static final String TIMEOUT_SYSTEM_PROPERTY = "spiffe.newJwtSource.timeout";
static final Duration DEFAULT_TIMEOUT =
Duration.parse(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, "PT0S"));
private JwtBundleSet bundles;
private final WorkloadApiClient workloadApiClient;
private volatile boolean closed;
// private constructor
private DefaultJwtSource(final WorkloadApiClient workloadApiClient) {
this.workloadApiClient = workloadApiClient;
}
/**
* Creates a new JWT source. It blocks until the initial update with the JWT bundles
* has been received from the Workload API or until the timeout configured
* through the system property `spiffe.newJwtSource.timeout` expires.
* If no timeout is configured, it blocks until it gets a JWT update from the Workload API.
* <p>
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
*
* @return an instance of {@link DefaultJwtSource}, with the JWT bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws JwtSourceException if the source could not be initialized
*/
public static JwtSource newSource() throws JwtSourceException, SocketEndpointAddressException {
JwtSourceOptions options = JwtSourceOptions.builder().initTimeout(DEFAULT_TIMEOUT).build();
return newSource(options);
}
/**
* Creates a new JWT source. It blocks until the initial update with the JWT bundles
* has been received from the Workload API, doing retries with a backoff exponential policy,
* or until the initTimeout has expired.
* <p>
* If the timeout is not provided in the options, the default timeout is read from the
* system property `spiffe.newJwtSource.timeout`. If none is configured, this method will
* block until the JWT bundles can be retrieved from the Workload API.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
*
* @param options {@link JwtSourceOptions}
* @return an instance of {@link DefaultJwtSource}, with the JWT bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws JwtSourceException if the source could not be initialized
*/
public static JwtSource newSource(@NonNull final JwtSourceOptions options)
throws SocketEndpointAddressException, JwtSourceException {
if (options.getWorkloadApiClient()== null) {
options.setWorkloadApiClient(createClient(options));
}
if (options.getInitTimeout()== null) {
options.setInitTimeout(DEFAULT_TIMEOUT);
}
DefaultJwtSource jwtSource = new DefaultJwtSource(options.getWorkloadApiClient());
try {
jwtSource.init(options.getInitTimeout());
} catch (Exception e) {
jwtSource.close();
throw new JwtSourceException("Error creating JWT source", e);
}
return jwtSource;
}
@Override
public JwtSvid fetchJwtSvid(String audience, String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvid(audience, extraAudiences);
}
/**
* Fetches a new JWT SVID from the Workload API for the given subject SPIFFE ID and audiences.
*
* @return a {@link JwtSvid}
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtSvid fetchJwtSvid(final SpiffeId subject, final String audience, final String... extraAudiences)
throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvid(subject, audience, extraAudiences);
}
@Override
public List<JwtSvid> fetchJwtSvids(String audience, String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvids(audience, extraAudiences);
}
/**
* Fetches all new JWT SVIDs from the Workload API for the given subject SPIFFE ID and audiences.
*
* @return all {@link JwtSvid}s
* @throws IllegalStateException if the source is closed
*/
@Override
public List<JwtSvid> fetchJwtSvids(final SpiffeId subject, final String audience, final String... extraAudiences)
throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvids(subject, audience, extraAudiences);
}
/**
* Returns the JWT bundle for a given trust domain.
*
* @return an instance of a {@link X509Bundle}
*
* @throws BundleNotFoundException is there is no bundle for the trust domain provided
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtBundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("JWT bundle source is closed");
}
return bundles.getBundleForTrustDomain(trustDomain);
}
/**
* Closes this source, dropping the connection to the Workload API.
* Other source methods will return an error after close has been called.
* <p>
* It is marked with {@link SneakyThrows} because it is not expected to throw
* the checked exception defined on the {@link Closeable} interface.
*/
@SneakyThrows
@Override
public void close() {
if (!closed) {
synchronized (this) {
if (!closed) {
workloadApiClient.close();
closed = true;
}
}
}
}
private void init(final Duration timeout) throws TimeoutException {
CountDownLatch done = new CountDownLatch(1);
setJwtBundlesWatcher(done);
boolean success;
if (timeout.isZero()) {
await(done);
success = true;
} else {
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
}
if (!success) {
throw new TimeoutException("Timeout waiting for JWT bundles update");
}
}
private void setJwtBundlesWatcher(final CountDownLatch done) {
workloadApiClient.watchJwtBundles(new Watcher<JwtBundleSet>() {
@Override
public void onUpdate(final JwtBundleSet update) {
log.log(Level.INFO, "Received JwtBundleSet update");
setJwtBundleSet(update);
done.countDown();
}
@Override
public void onError(final Throwable error) {
log.log(Level.SEVERE, "Error in JwtBundleSet watcher", error);
done.countDown();
throw new WatcherException("Error fetching JwtBundleSet", error);
}
});
}
private void setJwtBundleSet(final JwtBundleSet update) {
synchronized (this) {
this.bundles = update;
}
}
private boolean isClosed() {
synchronized (this) {
return closed;
}
}
private static WorkloadApiClient createClient(final JwtSourceOptions options)
throws SocketEndpointAddressException {
val clientOptions = DefaultWorkloadApiClient.ClientOptions
.builder()
.spiffeSocketPath(options.getSpiffeSocketPath())
.build();
return DefaultWorkloadApiClient.newClient(clientOptions);
}
}

View File

@ -0,0 +1,486 @@
package io.spiffe.workloadapi;
import io.grpc.Context;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.exception.X509ContextException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.svid.jwtsvid.JwtSvid;
import io.spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import io.spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
import io.spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
import io.spiffe.workloadapi.grpc.Workload;
import io.spiffe.workloadapi.internal.GrpcManagedChannelFactory;
import io.spiffe.workloadapi.internal.ManagedChannelWrapper;
import io.spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import io.spiffe.workloadapi.retry.ExponentialBackoffPolicy;
import io.spiffe.workloadapi.retry.RetryHandler;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.java.Log;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import static io.spiffe.workloadapi.StreamObservers.getJwtBundleStreamObserver;
import static io.spiffe.workloadapi.StreamObservers.getX509BundlesStreamObserver;
import static io.spiffe.workloadapi.StreamObservers.getX509ContextStreamObserver;
import static org.apache.commons.lang3.StringUtils.EMPTY;
/**
* Represents a client to interact with the Workload API.
* <p>
* Supports one-shot calls and watch updates for X.509 and JWT SVIDs and bundles.
* <p>
* The watch for updates methods support retries using an exponential backoff policy to reestablish
* the stream connection to the Workload API.
*/
@Log
public final class DefaultWorkloadApiClient implements WorkloadApiClient {
private final SpiffeWorkloadAPIStub workloadApiAsyncStub;
private final SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub;
private final ManagedChannelWrapper managedChannel;
private final List<Context.CancellableContext> cancellableContexts;
private final ExponentialBackoffPolicy exponentialBackoffPolicy;
// using a scheduled executor service to be able to schedule retries
// it is injected in each of the retryHandlers in the watch methods
private final ScheduledExecutorService retryExecutor;
private final ExecutorService executorService;
private volatile boolean closed;
private DefaultWorkloadApiClient(final SpiffeWorkloadAPIStub workloadApiAsyncStub,
final SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
final ManagedChannelWrapper managedChannel,
final ExponentialBackoffPolicy exponentialBackoffPolicy,
final ScheduledExecutorService retryExecutor,
final ExecutorService executorService) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.managedChannel = managedChannel;
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
this.exponentialBackoffPolicy = exponentialBackoffPolicy;
this.retryExecutor = retryExecutor;
this.executorService = executorService;
}
DefaultWorkloadApiClient(final SpiffeWorkloadAPIStub workloadApiAsyncStub,
final SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
final ManagedChannelWrapper managedChannel,
final ExponentialBackoffPolicy backoffPolicy) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.exponentialBackoffPolicy = backoffPolicy;
this.executorService = Executors.newCachedThreadPool();
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
this.managedChannel = managedChannel;
}
/**
* Creates a new Workload API client using the default socket endpoint address.
* {@link Address#getDefaultAddress()}
*
* @return a {@link WorkloadApiClient}, the instance concrete type is {@link DefaultWorkloadApiClient}
* @throws SocketEndpointAddressException if the Workload API socket endpoint address is not valid
*/
public static WorkloadApiClient newClient() throws SocketEndpointAddressException {
val options = ClientOptions.builder().build();
return newClient(options);
}
/**
* Creates a new Workload API client configured with the given client options.
* <p>
* If the SPIFFE socket endpoint address is not provided in the options, it uses the default address.
* {@link Address#getDefaultAddress()}
*
* @param options {@link ClientOptions}
* @return a {@link WorkloadApiClient}, the instance concrete type is {@link DefaultWorkloadApiClient}
* @throws SocketEndpointAddressException if the Workload API socket endpoint address is not valid
*/
public static WorkloadApiClient newClient(@NonNull final ClientOptions options) throws SocketEndpointAddressException {
val spiffeSocketPath = StringUtils.isNotBlank(options.spiffeSocketPath)
? options.spiffeSocketPath
: Address.getDefaultAddress();
if (options.exponentialBackoffPolicy == null) {
options.exponentialBackoffPolicy = ExponentialBackoffPolicy.DEFAULT;
}
if (options.executorService == null) {
options.executorService = Executors.newCachedThreadPool();
}
val socketEndpointAddress = Address.parseAddress(spiffeSocketPath);
val managedChannel = GrpcManagedChannelFactory.newChannel(socketEndpointAddress, options.executorService);
val securityHeaderInterceptor = new SecurityHeaderInterceptor();
val workloadAPIAsyncStub = SpiffeWorkloadAPIGrpc
.newStub(managedChannel.getChannel())
.withExecutor(options.executorService)
.withInterceptors(securityHeaderInterceptor);
val workloadAPIBlockingStub = SpiffeWorkloadAPIGrpc
.newBlockingStub(managedChannel.getChannel())
.withExecutor(options.executorService)
.withInterceptors(securityHeaderInterceptor);
val retryExecutor = Executors.newSingleThreadScheduledExecutor();
return new DefaultWorkloadApiClient(
workloadAPIAsyncStub,
workloadAPIBlockingStub,
managedChannel,
options.exponentialBackoffPolicy,
retryExecutor,
options.executorService);
}
/**
* {@inheritDoc}
*/
@Override
public X509Context fetchX509Context() throws X509ContextException {
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(this::callFetchX509Context);
} catch (Exception e) {
throw new X509ContextException("Error fetching X509Context", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void watchX509Context(@NonNull final Watcher<X509Context> watcher) {
val retryHandler = new RetryHandler(exponentialBackoffPolicy, retryExecutor);
val cancellableContext = Context.current().withCancellation();
val streamObserver =
getX509ContextStreamObserver(watcher, retryHandler, cancellableContext, workloadApiAsyncStub);
cancellableContext.run(() -> workloadApiAsyncStub.fetchX509SVID(newX509SvidRequest(), streamObserver));
this.cancellableContexts.add(cancellableContext);
}
/**
* {@inheritDoc}
*/
@Override
public X509BundleSet fetchX509Bundles() throws X509BundleException {
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(this::callFetchX509Bundles);
} catch (Exception e) {
throw new X509BundleException("Error fetching X.509 bundles", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void watchX509Bundles(@NonNull final Watcher<X509BundleSet> watcher) {
val retryHandler = new RetryHandler(exponentialBackoffPolicy, retryExecutor);
val cancellableContext = Context.current().withCancellation();
val streamObserver =
getX509BundlesStreamObserver(watcher, retryHandler, cancellableContext, workloadApiAsyncStub);
cancellableContext.run(() -> workloadApiAsyncStub.fetchX509Bundles(newX509BundlesRequest(), streamObserver));
this.cancellableContexts.add(cancellableContext);
}
/**
* {@inheritDoc}
*/
@Override
public JwtSvid fetchJwtSvid(@NonNull String audience, String... extraAudience) throws JwtSvidException {
final Set<String> audParam = createAudienceSet(audience, extraAudience);
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(() -> callFetchJwtSvid(audParam));
} catch (Exception e) {
throw new JwtSvidException("Error fetching JWT SVID", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public JwtSvid fetchJwtSvid(@NonNull final SpiffeId subject,
@NonNull final String audience,
final String... extraAudience)
throws JwtSvidException {
final Set<String> audParam = createAudienceSet(audience, extraAudience);
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(() -> callFetchJwtSvid(subject, audParam));
} catch (Exception e) {
throw new JwtSvidException("Error fetching JWT SVID", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public List<JwtSvid> fetchJwtSvids(@NonNull String audience, String... extraAudience) throws JwtSvidException {
final Set<String> audParam = createAudienceSet(audience, extraAudience);
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(() -> callFetchJwtSvids(audParam));
} catch (Exception e) {
throw new JwtSvidException("Error fetching JWT SVID", e);
}
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public List<JwtSvid> fetchJwtSvids(@NonNull final SpiffeId subject,
@NonNull final String audience,
final String... extraAudience)
throws JwtSvidException {
final Set<String> audParam = createAudienceSet(audience, extraAudience);
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(() -> callFetchJwtSvids(subject, audParam));
} catch (Exception e) {
throw new JwtSvidException("Error fetching JWT SVID", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public JwtBundleSet fetchJwtBundles() throws JwtBundleException {
try (val cancellableContext = Context.current().withCancellation()) {
return cancellableContext.call(this::callFetchBundles);
} catch (Exception e) {
throw new JwtBundleException("Error fetching JWT Bundles", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public JwtSvid validateJwtSvid(@NonNull final String token, @NonNull final String audience)
throws JwtSvidException {
val request = createJwtSvidRequest(token, audience);
Workload.ValidateJWTSVIDResponse response;
try (val cancellableContext = Context.current().withCancellation()) {
response = cancellableContext.call(() -> workloadApiBlockingStub.validateJWTSVID(request));
} catch (Exception e) {
throw new JwtSvidException("Error validating JWT SVID", e);
}
if (response == null || StringUtils.isBlank(response.getSpiffeId())) {
throw new JwtSvidException("Error validating JWT SVID. Empty response from Workload API");
}
return JwtSvid.parseInsecure(token, Collections.singleton(audience), EMPTY);
}
/**
* {@inheritDoc}
*/
@Override
public void watchJwtBundles(@NonNull final Watcher<JwtBundleSet> watcher) {
val retryHandler = new RetryHandler(exponentialBackoffPolicy, retryExecutor);
val cancellableContext = Context.current().withCancellation();
val streamObserver = getJwtBundleStreamObserver(watcher, retryHandler, cancellableContext, workloadApiAsyncStub);
cancellableContext.run(() -> workloadApiAsyncStub.fetchJWTBundles(newJwtBundlesRequest(), streamObserver));
this.cancellableContexts.add(cancellableContext);
}
/**
* Closes this Workload API closing the underlying channel,
* cancelling the contexts and shutdown the executor service.
*/
@Override
public void close() {
log.log(Level.FINE, "Closing WorkloadAPI client");
synchronized (this) {
if (!closed) {
for (val context : cancellableContexts) {
context.close();
}
if (managedChannel != null) {
managedChannel.close();
}
retryExecutor.shutdown();
executorService.shutdown();
closed = true;
}
}
log.log(Level.INFO, "WorkloadAPI client is closed");
}
private X509Context callFetchX509Context() throws X509ContextException {
val x509SvidResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
return GrpcConversionUtils.toX509Context(x509SvidResponse);
}
private X509BundleSet callFetchX509Bundles() throws X509BundleException {
val x509BundlesResponse = workloadApiBlockingStub.fetchX509Bundles(newX509BundlesRequest());
return GrpcConversionUtils.toX509BundleSet(x509BundlesResponse);
}
private JwtSvid callFetchJwtSvid(final SpiffeId subject, final Set<String> audience) throws JwtSvidException {
val jwtSvidRequest = Workload.JWTSVIDRequest.newBuilder()
.setSpiffeId(subject.toString())
.addAllAudience(audience)
.build();
val response = workloadApiBlockingStub.fetchJWTSVID(jwtSvidRequest);
return processJwtSvidResponse(response, audience, true).get(0);
}
private JwtSvid callFetchJwtSvid(final Set<String> audience) throws JwtSvidException {
val jwtSvidRequest = Workload.JWTSVIDRequest.newBuilder()
.addAllAudience(audience)
.build();
val response = workloadApiBlockingStub.fetchJWTSVID(jwtSvidRequest);
return processJwtSvidResponse(response, audience, true).get(0);
}
private List<JwtSvid> callFetchJwtSvids(final SpiffeId subject, final Set<String> audience) throws JwtSvidException {
val jwtSvidRequest = Workload.JWTSVIDRequest.newBuilder()
.setSpiffeId(subject.toString())
.addAllAudience(audience)
.build();
val response = workloadApiBlockingStub.fetchJWTSVID(jwtSvidRequest);
return processJwtSvidResponse(response, audience, false);
}
private List<JwtSvid> callFetchJwtSvids(final Set<String> audience) throws JwtSvidException {
val jwtSvidRequest = Workload.JWTSVIDRequest.newBuilder()
.addAllAudience(audience)
.build();
val response = workloadApiBlockingStub.fetchJWTSVID(jwtSvidRequest);
return processJwtSvidResponse(response, audience, false);
}
private List<JwtSvid> processJwtSvidResponse(Workload.JWTSVIDResponse response, Set<String> audience, boolean firstOnly) throws JwtSvidException {
if (response.getSvidsList() == null || response.getSvidsList().isEmpty()) {
throw new JwtSvidException("JWT SVID response from the Workload API is empty");
}
int n = response.getSvidsCount();
if (firstOnly) {
n = 1;
}
ArrayList<JwtSvid> svids = new ArrayList<>(n);
HashSet<String> hints = new HashSet<>();
for (int i = 0; i < n; i++) {
// In the event of more than one JWTSVID message with the same hint value set, then the first message in the
// list SHOULD be selected.
if (hints.contains(response.getSvids(i).getHint())) {
continue;
}
val svid = JwtSvid.parseInsecure(response.getSvids(i).getSvid(), audience, response.getSvids(i).getHint());
hints.add(svid.getHint());
svids.add(svid);
}
return svids;
}
private JwtBundleSet callFetchBundles() throws JwtBundleException {
val request = Workload.JWTBundlesRequest.newBuilder().build();
val bundlesResponse = workloadApiBlockingStub.fetchJWTBundles(request);
return GrpcConversionUtils.toJwtBundleSet(bundlesResponse);
}
private Set<String> createAudienceSet(final String audience, final String[] extraAudience) {
final Set<String> audParam = new HashSet<>();
audParam.add(audience);
Collections.addAll(audParam, extraAudience);
return audParam;
}
private Workload.X509SVIDRequest newX509SvidRequest() {
return Workload.X509SVIDRequest.newBuilder().build();
}
private Workload.X509BundlesRequest newX509BundlesRequest() {
return Workload.X509BundlesRequest.newBuilder().build();
}
private Workload.JWTBundlesRequest newJwtBundlesRequest() {
return Workload.JWTBundlesRequest.newBuilder().build();
}
private Workload.ValidateJWTSVIDRequest createJwtSvidRequest(final String token, final String audience) {
return Workload.ValidateJWTSVIDRequest
.newBuilder()
.setSvid(token)
.setAudience(audience)
.build();
}
/**
* Options for creating a new {@link DefaultWorkloadApiClient}.
* <p>
* <code>spiffeSocketPath</code> Workload API Socket Endpoint address.
* <p>
* <code>backoffPolicy</code> A custom instance of a {@link ExponentialBackoffPolicy} to configure the retries to reconnect
* to the Workload API.
* <p>
* <code>executorService</code> A custom {@link ExecutorService} to configure the Grpc stubs and channels.
* If it is not provided, an <code>Executors.newCachedThreadPool()</code> is used by default.
* The executorService provided will be shutdown when the WorkloadApiClient instance is closed.
*/
@Data
public static class ClientOptions {
@Setter(AccessLevel.NONE)
private String spiffeSocketPath;
@Setter(AccessLevel.NONE)
private ExponentialBackoffPolicy exponentialBackoffPolicy;
@Setter(AccessLevel.NONE)
private ExecutorService executorService;
@Builder
public ClientOptions(final String spiffeSocketPath,
final ExponentialBackoffPolicy exponentialBackoffPolicy,
final ExecutorService executorService) {
this.spiffeSocketPath = spiffeSocketPath;
this.exponentialBackoffPolicy = exponentialBackoffPolicy;
this.executorService = executorService;
}
}
}

View File

@ -0,0 +1,271 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.exception.WatcherException;
import io.spiffe.exception.X509SourceException;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.x509svid.X509Svid;
import io.spiffe.svid.x509svid.X509SvidSource;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import lombok.val;
import java.io.Closeable;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static io.spiffe.workloadapi.internal.ThreadUtils.await;
/**
* Represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API.
* <p>
* It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically
* whenever there is an update from the Workload API.
* <p>
* Implements {@link X509SvidSource} and {@link BundleSource}.
* <p>
* Implements the {@link Closeable} interface. The {@link #close()} method closes the source,
* dropping the connection to the Workload API. Other source methods will return an error
* after close has been called.
*/
@Log
public final class DefaultX509Source implements X509Source {
private static final String TIMEOUT_SYSTEM_PROPERTY = "spiffe.newX509Source.timeout";
private static final Duration DEFAULT_TIMEOUT = Duration.parse(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, "PT0S"));
private X509Svid svid;
private X509BundleSet bundles;
private final Function<List<X509Svid>, X509Svid> picker;
private final WorkloadApiClient workloadApiClient;
private volatile boolean closed;
// private constructor
private DefaultX509Source(final Function<List<X509Svid>, X509Svid> svidPicker, final WorkloadApiClient workloadApiClient) {
this.picker = svidPicker;
this.workloadApiClient = workloadApiClient;
}
/**
* Creates a new X.509 source. It blocks until the initial update with the X.509 materials
* has been received from the Workload API or until the timeout configured
* through the system property `spiffe.newX509Source.timeout` expires.
* If no timeout is configured, it blocks until it gets an X.509 update from the Workload API.
* <p>
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
* <p>
* It uses the default X.509 SVID (picks the first SVID that comes in the Workload API response).
*
* @return an instance of {@link DefaultX509Source}, with the SVID and bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
public static DefaultX509Source newSource() throws SocketEndpointAddressException, X509SourceException {
val x509SourceOptions = X509SourceOptions.builder().initTimeout(DEFAULT_TIMEOUT).build();
return newSource(x509SourceOptions);
}
/**
* Creates a new X.509 source. It blocks until the initial update with the X.509 materials
* has been received from the Workload API, doing retries with a backoff exponential policy,
* or until the timeout has expired.
* <p>
* If the timeout is not provided in the options, the default timeout is read from the
* system property `spiffe.newX509Source.timeout`. If none is configured, this method will
* block until the X.509 materials can be retrieved from the Workload API.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
* <p>
* If no SVID Picker is provided in the options, it uses the default X.509 SVID (picks the first SVID that comes
* in the Workload API response).
*
* @param options {@link X509SourceOptions}
* @return an instance of {@link DefaultX509Source}, with the SVID and bundles initialized
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
public static DefaultX509Source newSource(@NonNull final X509SourceOptions options)
throws SocketEndpointAddressException, X509SourceException {
if (options.workloadApiClient == null) {
options.workloadApiClient = createClient(options);
}
if (options.initTimeout == null) {
options.initTimeout = DEFAULT_TIMEOUT;
}
val x509Source = new DefaultX509Source(options.svidPicker, options.workloadApiClient);
try {
x509Source.init(options.initTimeout);
} catch (Exception e) {
x509Source.close();
throw new X509SourceException("Error creating X.509 source", e);
}
return x509Source;
}
/**
* Returns the X.509 SVID handled by this source.
*
* @return a {@link X509Svid}
* @throws IllegalStateException if the source is closed
*/
@Override
public X509Svid getX509Svid() {
if (isClosed()) {
throw new IllegalStateException("X.509 SVID source is closed");
}
return svid;
}
/**
* Returns the X.509 bundle for a given trust domain.
*
* @return an instance of a {@link X509Bundle}
* @throws BundleNotFoundException is there is no bundle for the trust domain provided
* @throws IllegalStateException if the source is closed
*/
@Override
public X509Bundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("X.509 bundle source is closed");
}
return bundles.getBundleForTrustDomain(trustDomain);
}
/**
* Closes this source, dropping the connection to the Workload API.
* Other source methods will return an error after close has been called.
* <p>
* It is marked with {@link SneakyThrows} because it is not expected to throw
* the checked exception defined on the {@link Closeable} interface.
*/
@SneakyThrows
@Override
public void close() {
if (!closed) {
synchronized (this) {
if (!closed) {
workloadApiClient.close();
closed = true;
}
}
}
}
private static WorkloadApiClient createClient(final X509SourceOptions options)
throws SocketEndpointAddressException {
val clientOptions = DefaultWorkloadApiClient.ClientOptions
.builder()
.spiffeSocketPath(options.spiffeSocketPath)
.build();
return DefaultWorkloadApiClient.newClient(clientOptions);
}
private void init(final Duration timeout) throws TimeoutException {
val done = new CountDownLatch(1);
setX509ContextWatcher(done);
final boolean success;
if (timeout.isZero()) {
await(done);
success = true;
} else {
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
}
if (!success) {
throw new TimeoutException("Timeout waiting for X.509 Context update");
}
}
private void setX509ContextWatcher(final CountDownLatch done) {
workloadApiClient.watchX509Context(new Watcher<X509Context>() {
@Override
public void onUpdate(final X509Context update) {
String spiffeIds = update.getX509Svids().stream().map(s -> s.getSpiffeId().toString()).collect(Collectors.joining(", "));
log.log(Level.INFO, String.format("Received X509Context update: %s", spiffeIds));
setX509Context(update);
done.countDown();
}
@Override
public void onError(final Throwable error) {
log.log(Level.SEVERE, "Error in X509Context watcher", error);
done.countDown();
throw new WatcherException("Error in X509Context watcher", error);
}
});
}
private void setX509Context(final X509Context update) {
final X509Svid svidUpdate;
if (picker == null) {
svidUpdate = update.getDefaultSvid();
} else {
svidUpdate = picker.apply(update.getX509Svids());
}
synchronized (this) {
this.svid = svidUpdate;
this.bundles = update.getX509BundleSet();
}
}
private boolean isClosed() {
synchronized (this) {
return closed;
}
}
/**
* Options for creating a new {@link DefaultX509Source}
* <p>
* <code>spiffeSocketPath</code> Address to the Workload API, if it is not set, the default address will be used.
* <p>
* <code>initTimeout</code> Timeout for initializing the instance. If it is not defined, the timeout is read
* from the System property `spiffe.newX509Source.timeout'. If this is also not defined, no default timeout is applied.
* <p>
* <code>svidPicker</code> Function to choose the X.509 SVID from the list returned by the Workload API.
* If it is not set, the default SVID is picked.
* <p>
* <code>workloadApiClient</code> A custom instance of a {@link WorkloadApiClient}, if it is not set, a new client
* will be created.
*/
@Data
public static class X509SourceOptions {
private String spiffeSocketPath;
private Duration initTimeout;
private Function<List<X509Svid>, X509Svid> svidPicker;
private WorkloadApiClient workloadApiClient;
@Builder
public X509SourceOptions(final String spiffeSocketPath,
final Duration initTimeout,
final Function<List<X509Svid>, X509Svid> svidPicker,
final WorkloadApiClient workloadApiClient) {
this.spiffeSocketPath = spiffeSocketPath;
this.initTimeout = initTimeout;
this.svidPicker = svidPicker;
this.workloadApiClient = workloadApiClient;
}
}
}

View File

@ -0,0 +1,186 @@
package io.spiffe.workloadapi;
import com.google.protobuf.ByteString;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.exception.X509ContextException;
import io.spiffe.exception.X509SvidException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.x509svid.X509Svid;
import io.spiffe.workloadapi.grpc.Workload;
import lombok.val;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
/**
* Utility methods for converting GRPC objects to JAVA-SPIFFE domain objects.
*/
final class GrpcConversionUtils {
private GrpcConversionUtils() {
}
static X509Context toX509Context(final Iterator<Workload.X509SVIDResponse> x509SvidResponseIterator) throws X509ContextException {
if (!x509SvidResponseIterator.hasNext()) {
throw new X509ContextException("X.509 Context response from the Workload API is empty");
}
val x509SvidResponse = x509SvidResponseIterator.next();
return toX509Context(x509SvidResponse);
}
static X509Context toX509Context(final Workload.X509SVIDResponse x509SvidResponse) throws X509ContextException {
if (x509SvidResponse.getSvidsList() == null || x509SvidResponse.getSvidsList().isEmpty()) {
throw new X509ContextException("X.509 Context response from the Workload API is empty");
}
val x509SvidList = getListOfX509Svid(x509SvidResponse);
val x509BundleList = getListOfX509Bundles(x509SvidResponse);
val bundleSet = X509BundleSet.of(x509BundleList);
return X509Context.of(x509SvidList, bundleSet);
}
public static X509BundleSet toX509BundleSet(Iterator<Workload.X509BundlesResponse> bundlesResponseIterator) throws X509BundleException {
if (!bundlesResponseIterator.hasNext()) {
throw new X509BundleException("X.509 Bundle response from the Workload API is empty");
}
val bundlesResponse = bundlesResponseIterator.next();
return toX509BundleSet(bundlesResponse);
}
static X509BundleSet toX509BundleSet(final Workload.X509BundlesResponse bundlesResponse) throws X509BundleException {
val bundlesCount = bundlesResponse.getBundlesCount();
if (bundlesCount == 0) {
throw new X509BundleException("X.509 Bundle response from the Workload API is empty");
}
final List<X509Bundle> x509Bundles = new ArrayList<>(bundlesCount);
for (Map.Entry<String, ByteString> entry : bundlesResponse.getBundlesMap().entrySet()) {
X509Bundle x509Bundle = createX509Bundle(entry);
x509Bundles.add(x509Bundle);
}
return X509BundleSet.of(x509Bundles);
}
static JwtBundleSet toJwtBundleSet(final Iterator<Workload.JWTBundlesResponse> bundlesResponseIterator) throws JwtBundleException {
if (!bundlesResponseIterator.hasNext()) {
throw new JwtBundleException("JWT Bundle response from the Workload API is empty");
}
val bundlesResponse = bundlesResponseIterator.next();
return toJwtBundleSet(bundlesResponse);
}
static JwtBundleSet toJwtBundleSet(final Workload.JWTBundlesResponse bundlesResponse) throws JwtBundleException {
if (bundlesResponse.getBundlesMap().size() == 0) {
throw new JwtBundleException("JWT Bundle response from the Workload API is empty");
}
final List<JwtBundle> jwtBundles = new ArrayList<>();
for (Map.Entry<String, ByteString> entry : bundlesResponse.getBundlesMap().entrySet()) {
JwtBundle jwtBundle = createJwtBundle(entry);
jwtBundles.add(jwtBundle);
}
return JwtBundleSet.of(jwtBundles);
}
static X509Bundle parseX509Bundle(TrustDomain trustDomain, byte[] bundleBytes) throws X509ContextException {
try {
return X509Bundle.parse(trustDomain, bundleBytes);
} catch (X509BundleException e) {
throw new X509ContextException("X.509 Bundles could not be processed", e);
}
}
private static List<X509Bundle> getListOfX509Bundles(final Workload.X509SVIDResponse x509SvidResponse) throws X509ContextException {
final List<X509Bundle> x509BundleList = new ArrayList<>();
for (Workload.X509SVID x509Svid : x509SvidResponse.getSvidsList()) {
X509Bundle bundle = createX509Bundle(x509Svid);
x509BundleList.add(bundle);
}
// Process federated bundles
Set<Map.Entry<String, ByteString>> federatedBundles = x509SvidResponse.getFederatedBundlesMap().entrySet();
for (Map.Entry<String, ByteString> bundleEntry : federatedBundles) {
TrustDomain trustDomain = TrustDomain.parse(bundleEntry.getKey());
byte[] bundleBytes = bundleEntry.getValue().toByteArray();
val bundle = parseX509Bundle(trustDomain, bundleBytes);
x509BundleList.add(bundle);
}
return x509BundleList;
}
private static X509Bundle createX509Bundle(Workload.X509SVID x509Svid) throws X509ContextException {
val spiffeId = SpiffeId.parse(x509Svid.getSpiffeId());
TrustDomain trustDomain = spiffeId.getTrustDomain();
byte[] bundleBytes = x509Svid.getBundle().toByteArray();
return parseX509Bundle(trustDomain, bundleBytes);
}
private static List<X509Svid> getListOfX509Svid(final Workload.X509SVIDResponse x509SvidResponse) throws X509ContextException{
final List<X509Svid> result = new ArrayList<>();
HashSet<String> hints = new HashSet<>();
for (Workload.X509SVID x509SVID : x509SvidResponse.getSvidsList()) {
// In the event of more than one X509SVID message with the same hint value set, then the first message in the
// list SHOULD be selected.
if (hints.contains(x509SVID.getHint())) {
continue;
}
val svid = createAndValidateX509Svid(x509SVID);
hints.add(svid.getHint());
result.add(svid);
}
return result;
}
private static X509Svid createAndValidateX509Svid(Workload.X509SVID x509SVID) throws X509ContextException {
byte[] certsBytes = x509SVID.getX509Svid().toByteArray();
byte[] privateKeyBytes = x509SVID.getX509SvidKey().toByteArray();
X509Svid svid;
try {
svid = X509Svid.parseRaw(certsBytes, privateKeyBytes, x509SVID.getHint());
} catch (X509SvidException e) {
throw new X509ContextException("X.509 SVID response could not be processed", e);
}
val spiffeIdResponse = x509SVID.getSpiffeId();
val spiffeIdSvid = svid.getSpiffeId();
validateSpiffeId(spiffeIdSvid.toString(), spiffeIdResponse);
return svid;
}
private static void validateSpiffeId(String spiffeIdSvid, String spiffeIdResponse) throws X509ContextException {
if (!spiffeIdSvid.equals(spiffeIdResponse.trim())) {
val format = "SPIFFE ID in X509SVIDResponse (%s) does not match SPIFFE ID in X.509 certificate (%s)";
throw new X509ContextException(String.format(format, spiffeIdResponse, spiffeIdSvid));
}
}
private static JwtBundle createJwtBundle(Map.Entry<String, ByteString> entry) throws JwtBundleException {
TrustDomain trustDomain = TrustDomain.parse(entry.getKey());
byte[] bundleBytes = entry.getValue().toByteArray();
return JwtBundle.parse(trustDomain, bundleBytes);
}
private static X509Bundle createX509Bundle(Map.Entry<String, ByteString> bundleEntry) throws X509BundleException {
TrustDomain trustDomain = TrustDomain.parse(bundleEntry.getKey());
byte[] bundleBytes = bundleEntry.getValue().toByteArray();
return X509Bundle.parse(trustDomain, bundleBytes);
}
}

View File

@ -0,0 +1,16 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.svid.jwtsvid.JwtSvidSource;
import java.io.Closeable;
/**
* Source of JWT SVIDs and Bundles.
* @see JwtSvidSource
* @see BundleSource
* @see JwtBundle
*/
public interface JwtSource extends JwtSvidSource, BundleSource<JwtBundle>, Closeable {
}

View File

@ -0,0 +1,43 @@
package io.spiffe.workloadapi;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.Setter;
import java.time.Duration;
/**
* Options to configure a {@link JwtSource}.
* <p>
* <code>spiffeSocketPath</code> Address to the Workload API, if it is not set, the default address will be used.
* <p>
* <code>initTimeout</code> Timeout for initializing the instance. If it is not defined, the timeout is read
* from the System property `spiffe.newJwtSource.timeout'. If this is also not defined, no default timeout is applied.
* <p>
* <code>workloadApiClient</code> A custom instance of a {@link WorkloadApiClient}, if it is not set,
* a new client will be created.
*/
@Data
public class JwtSourceOptions {
@Setter(AccessLevel.PUBLIC)
private String spiffeSocketPath;
@Setter(AccessLevel.PUBLIC)
private Duration initTimeout;
@Setter(AccessLevel.PUBLIC)
private WorkloadApiClient workloadApiClient;
@Builder
public JwtSourceOptions(
final String spiffeSocketPath,
final WorkloadApiClient workloadApiClient,
final Duration initTimeout) {
this.spiffeSocketPath = spiffeSocketPath;
this.workloadApiClient = workloadApiClient;
this.initTimeout = initTimeout;
}
}

View File

@ -0,0 +1,204 @@
package io.spiffe.workloadapi;
import io.grpc.Context;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.exception.X509ContextException;
import io.spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import io.spiffe.workloadapi.grpc.Workload;
import io.spiffe.workloadapi.retry.RetryHandler;
import lombok.extern.java.Log;
import lombok.val;
import java.util.logging.Level;
@Log
final class StreamObservers {
private static final String INVALID_ARGUMENT = "INVALID_ARGUMENT";
private static final String STREAM_IS_COMPLETED = "Workload API stream is completed";
private StreamObservers() {
}
static StreamObserver<Workload.X509SVIDResponse> getX509ContextStreamObserver(
final Watcher<X509Context> watcher,
final RetryHandler retryHandler,
final Context.CancellableContext cancellableContext,
final SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub workloadApiAsyncStub) {
return new StreamObserver<Workload.X509SVIDResponse>() {
@Override
public void onNext(final Workload.X509SVIDResponse value) {
try {
val x509Context = GrpcConversionUtils.toX509Context(value);
watcher.onUpdate(x509Context);
retryHandler.reset();
} catch (X509ContextException e) {
watcher.onError(new X509ContextException("Error processing X.509 Context update", e));
}
}
@Override
public void onError(final Throwable t) {
if (Status.fromThrowable(t).getCode() != Status.Code.CANCELLED) {
log.log(Level.SEVERE, "X.509 context observer error", t);
}
handleWatchX509ContextError(t);
}
private void handleWatchX509ContextError(final Throwable t) {
if (isErrorNotRetryable(t)) {
watcher.onError(new X509ContextException("Cancelling X.509 Context watch", t));
} else {
handleX509ContextRetry(t);
}
}
private void handleX509ContextRetry(Throwable t) {
if (retryHandler.shouldRetry()) {
log.log(Level.FINE, "Retrying connecting to Workload API to register X.509 context watcher");
retryHandler.scheduleRetry(() ->
cancellableContext.run(
() -> workloadApiAsyncStub.fetchX509SVID(newX509SvidRequest(),
this)));
} else {
watcher.onError(new X509ContextException("Cancelling X.509 Context watch", t));
}
}
@Override
public void onCompleted() {
cancellableContext.close();
log.info(STREAM_IS_COMPLETED);
}
};
}
static StreamObserver<Workload.X509BundlesResponse> getX509BundlesStreamObserver(
final Watcher<X509BundleSet> watcher,
final RetryHandler retryHandler,
final Context.CancellableContext cancellableContext,
final SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub workloadApiAsyncStub) {
return new StreamObserver<Workload.X509BundlesResponse>() {
@Override
public void onNext(final Workload.X509BundlesResponse value) {
try {
val x509Context = GrpcConversionUtils.toX509BundleSet(value);
watcher.onUpdate(x509Context);
retryHandler.reset();
} catch (X509BundleException e) {
watcher.onError(new X509ContextException("Error processing X.509 bundles update", e));
}
}
@Override
public void onError(final Throwable t) {
if (Status.fromThrowable(t).getCode() != Status.Code.CANCELLED) {
log.log(Level.SEVERE, "X.509 bundles observer error", t);
}
handleWatchX509BundlesError(t);
}
private void handleWatchX509BundlesError(final Throwable t) {
if (isErrorNotRetryable(t)) {
watcher.onError(new X509ContextException("Cancelling X.509 bundles watch", t));
} else {
handleX509BundlesRetry(t);
}
}
private void handleX509BundlesRetry(Throwable t) {
if (retryHandler.shouldRetry()) {
log.log(Level.FINE, "Retrying connecting to Workload API to register X.509 bundles watcher");
retryHandler.scheduleRetry(() ->
cancellableContext.run(
() -> workloadApiAsyncStub.fetchX509Bundles(newX509BundlesRequest(),
this)));
} else {
watcher.onError(new X509BundleException("Cancelling X.509 bundles watch", t));
}
}
@Override
public void onCompleted() {
cancellableContext.close();
log.info(STREAM_IS_COMPLETED);
}
};
}
static StreamObserver<Workload.JWTBundlesResponse> getJwtBundleStreamObserver(
final Watcher<JwtBundleSet> watcher,
final RetryHandler retryHandler,
final Context.CancellableContext cancellableContext,
final SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub workloadApiAsyncStub) {
return new StreamObserver<Workload.JWTBundlesResponse>() {
@Override
public void onNext(final Workload.JWTBundlesResponse value) {
try {
val jwtBundleSet = GrpcConversionUtils.toJwtBundleSet(value);
watcher.onUpdate(jwtBundleSet);
retryHandler.reset();
} catch (JwtBundleException e) {
watcher.onError(new JwtBundleException("Error processing JWT bundles update", e));
}
}
@Override
public void onError(final Throwable t) {
if (Status.fromThrowable(t).getCode() != Status.Code.CANCELLED) {
log.log(Level.SEVERE, "JWT observer error", t);
}
handleWatchJwtBundleError(t);
}
private void handleWatchJwtBundleError(final Throwable t) {
if (isErrorNotRetryable(t)) {
watcher.onError(new JwtBundleException("Cancelling JWT Bundles watch", t));
} else {
handleJwtBundleRetry(t);
}
}
private void handleJwtBundleRetry(Throwable t) {
if (retryHandler.shouldRetry()) {
log.log(Level.FINE, "Retrying connecting to Workload API to register JWT Bundles watcher");
retryHandler.scheduleRetry(() ->
cancellableContext.run(() -> workloadApiAsyncStub.fetchJWTBundles(newJwtBundlesRequest(),
this)));
} else {
watcher.onError(new JwtBundleException("Cancelling JWT Bundles watch", t));
}
}
@Override
public void onCompleted() {
cancellableContext.close();
log.info(STREAM_IS_COMPLETED);
}
};
}
private static boolean isErrorNotRetryable(Throwable t) {
return INVALID_ARGUMENT.equals(Status.fromThrowable(t).getCode().name());
}
private static Workload.X509SVIDRequest newX509SvidRequest() {
return Workload.X509SVIDRequest.newBuilder().build();
}
private static Workload.X509BundlesRequest newX509BundlesRequest() {
return Workload.X509BundlesRequest.newBuilder().build();
}
private static Workload.JWTBundlesRequest newJwtBundlesRequest() {
return Workload.JWTBundlesRequest.newBuilder().build();
}
}

View File

@ -0,0 +1,21 @@
package io.spiffe.workloadapi;
/**
* Watches updates of type T.
*
* @param <T> is the type of the updates.
*/
public interface Watcher<T> {
/**
* Method called in case of success getting an update.
* @param update the instance of type T
*/
void onUpdate(final T update);
/**
* Method called in case there is an error watching for updates.
* @param e the throwable exception that was caught
*/
void onError(final Throwable e);
}

View File

@ -0,0 +1,131 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.jwtbundle.JwtBundleSet;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.exception.X509ContextException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.svid.jwtsvid.JwtSvid;
import lombok.NonNull;
import java.io.Closeable;
import java.util.List;
/**
* Represents a client to interact with the Workload API.
* <p>
* Supports one-shot calls and watch updates for X.509 and JWT SVIDs and bundles.
*/
public interface WorkloadApiClient extends Closeable {
/**
* Fetches an X.509 context on a one-shot blocking call.
*
* @return an instance of a {@link X509Context} containing the X.509 materials fetched from the Workload API
* @throws X509ContextException if there is an error fetching or processing the X.509 context
*/
X509Context fetchX509Context() throws X509ContextException;
/**
* Watches for X.509 context updates.
* <p>
* A new Stream to the Workload API is opened for each call to this method, so that the client starts getting
* updates immediately after the Stream is ready and doesn't have to wait until the Workload API dispatches
* the next update based on the SVIDs TTL.
*
* @param watcher an instance that implements a {@link Watcher}.
*/
void watchX509Context(@NonNull Watcher<X509Context> watcher);
/**
* Fetches the X.509 bundles on a one-shot blocking call.
*
* @return an instance of a {@link X509BundleSet} containing the X.509 bundles keyed by TrustDomain
* @throws X509BundleException if there is an error fetching or processing the X.509 bundles
*/
X509BundleSet fetchX509Bundles() throws X509BundleException;
/**
* Watches for X.509 bundles updates.
* <p>
* A new Stream to the Workload API is opened for each call to this method, so that the client starts getting
* updates immediately after the Stream is ready and doesn't have to wait until the Workload API dispatches
* the next update.
*
* @param watcher an instance that implements a {@link Watcher} for {@link X509BundleSet}.
*/
void watchX509Bundles(@NonNull Watcher<X509BundleSet> watcher);
/**
* Fetches a SPIFFE JWT-SVID on one-shot blocking call.
*
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return an instance of a {@link JwtSvid}
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
JwtSvid fetchJwtSvid(@NonNull String audience, String... extraAudience) throws JwtSvidException;
/**
* Fetches a SPIFFE JWT-SVID on one-shot blocking call.
*
* @param subject a SPIFFE ID
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return an instance of a {@link JwtSvid}
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
JwtSvid fetchJwtSvid(@NonNull SpiffeId subject, @NonNull String audience, String... extraAudience) throws JwtSvidException;
/**
* Fetches all SPIFFE JWT-SVIDs on one-shot blocking call.
*
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return all of {@link JwtSvid} object
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
List<JwtSvid> fetchJwtSvids(@NonNull String audience, String... extraAudience) throws JwtSvidException;
/**
* Fetches a SPIFFE JWT-SVID on one-shot blocking call.
*
* @param subject a SPIFFE ID
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return all of {@link JwtSvid} objectÏ
* @throws JwtSvidException if there is an error fetching or processing the JWT from the Workload API
*/
List<JwtSvid> fetchJwtSvids(@NonNull SpiffeId subject, @NonNull String audience, String... extraAudience) throws JwtSvidException;
/**
* Fetches the JWT bundles for JWT-SVID validation, keyed by trust domain.
*
* @return an instance of a {@link JwtBundleSet}
* @throws JwtBundleException when there is an error getting or processing the response from the Workload API
*/
JwtBundleSet fetchJwtBundles() throws JwtBundleException;
/**
* Validates the JWT-SVID token. The parsed and validated JWT-SVID is returned.
*
* @param token JWT token
* @param audience audience of the JWT
* @return a {@link JwtSvid} if the token and audience could be validated.
* @throws JwtSvidException when the token cannot be validated with the audience
*/
JwtSvid validateJwtSvid(@NonNull String token, @NonNull String audience) throws JwtSvidException;
/**
* Watches for JWT bundles updates.
* <p>
* A new Stream to the Workload API is opened for each call to this method, so that the client starts getting
* updates immediately after the Stream is ready and doesn't have to wait until the Workload API dispatches
* the next update based on the SVIDs TTL.
*
* @param watcher receives the update for JwtBundles.
*/
void watchJwtBundles(@NonNull Watcher<JwtBundleSet> watcher);
}

View File

@ -0,0 +1,48 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.svid.x509svid.X509Svid;
import lombok.NonNull;
import lombok.Value;
import java.util.List;
/**
* Represents the X.509 materials that are fetched from the Workload API.
* <p>
* Contains a list of {@link X509Svid} and a {@link X509BundleSet}.
*/
@Value
public class X509Context {
List<X509Svid> x509Svids;
X509BundleSet x509BundleSet;
X509Context(final List<X509Svid> x509Svids, final X509BundleSet x509BundleSet) {
this.x509Svids = x509Svids;
this.x509BundleSet = x509BundleSet;
}
/**
* Creates a new X509Context from the list of X.509 SVIDs and the X.509 Bundle set.
*
* @param x509Svids a list of {@link X509Svid}
* @param x509BundleSet an instance of {@link X509BundleSet}
* @return an instance of an X509Context
*/
public static X509Context of(@NonNull final List<X509Svid> x509Svids, @NonNull final X509BundleSet x509BundleSet) {
if (x509Svids.size() == 0) {
throw new IllegalArgumentException("The X.509 Context must have a least one X.509 SVID");
}
return new X509Context(x509Svids, x509BundleSet);
}
/**
* Returns the default SVID (the first in the list).
*
* @return the default SVID (the first in the list)
*/
public X509Svid getDefaultSvid() {
return x509Svids.get(0);
}
}

View File

@ -0,0 +1,16 @@
package io.spiffe.workloadapi;
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.svid.x509svid.X509SvidSource;
import java.io.Closeable;
/**
* Source of X.509 SVIDs and Bundles.
* @see X509SvidSource
* @see BundleSource
* @see X509Bundle
*/
public interface X509Source extends X509SvidSource, BundleSource<X509Bundle>, Closeable {
}

View File

@ -0,0 +1,33 @@
package io.spiffe.workloadapi.internal;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.ForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
/**
* ClientInterceptor implementation to add a security header required to connect to the Workload API.
*/
public class SecurityHeaderInterceptor implements ClientInterceptor {
private static final String SECURITY_HEADER = "workload.spiffe.io";
/**
* Intercepts the call to the Workload API and adds the required security header.
*/
@Override
public <R,S> ClientCall<R,S> interceptCall(MethodDescriptor<R,S> method, CallOptions callOptions, Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<R,S>(next.newCall(method, callOptions)) {
@Override
public void start(Listener<S> responseListener, Metadata headers) {
Metadata.Key<String> headerKey = Metadata.Key.of(SECURITY_HEADER, Metadata.ASCII_STRING_MARSHALLER);
headers.put(headerKey, "true");
super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener<S>(responseListener) {}, headers);
}
};
}
}

View File

@ -0,0 +1,30 @@
package io.spiffe.workloadapi.internal;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public final class ThreadUtils {
private ThreadUtils() {
}
public static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static boolean await(CountDownLatch latch, long timeout, TimeUnit unit) {
boolean result;
try {
result = latch.await(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
result = false;
}
return result;
}
}

View File

@ -0,0 +1,90 @@
package io.spiffe.workloadapi.retry;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.Setter;
import lombok.val;
import java.time.Duration;
import java.util.function.UnaryOperator;
/**
* Represents a backoff policy for performing retries using exponential increasing delays.
*/
@Data
public class ExponentialBackoffPolicy {
public static final ExponentialBackoffPolicy DEFAULT = new ExponentialBackoffPolicy();
// Retry indefinitely, default behavior.
public static final int UNLIMITED_RETRIES = 0;
private static final int BACKOFF_MULTIPLIER = 2;
// The first backoff delay period.
@Setter(AccessLevel.NONE)
private Duration initialDelay = Duration.ofSeconds(1);
// Max time of delay for the backoff period.
@Setter(AccessLevel.NONE)
private Duration maxDelay = Duration.ofSeconds(60);
// Max number of retries, unlimited by default.
@Setter(AccessLevel.NONE)
private int maxRetries = UNLIMITED_RETRIES;
@Setter(AccessLevel.NONE)
private int backoffMultiplier = BACKOFF_MULTIPLIER;
@Setter(AccessLevel.NONE)
private UnaryOperator<Duration> backoffFunction = d -> d.multipliedBy(backoffMultiplier);
@Builder
public ExponentialBackoffPolicy(final Duration initialDelay,
final Duration maxDelay,
final int maxRetries,
final int backoffMultiplier) {
this.initialDelay = initialDelay != null ? initialDelay : Duration.ofSeconds(1);
this.maxDelay = maxDelay != null ? maxDelay : Duration.ofSeconds(60);
this.maxRetries = maxRetries != 0 ? maxRetries : UNLIMITED_RETRIES;
this.backoffMultiplier = backoffMultiplier != 0 ? backoffMultiplier : BACKOFF_MULTIPLIER;
}
private ExponentialBackoffPolicy() {
}
/**
* Calculate the nextDelay based on a currentDelay, applying the backoff function
* If the calculated delay is greater than maxDelay, it returns maxDelay.
*
* @param currentDelay a {@link Duration} representing the current delay
* @return a {@link Duration} representing the next delay
*/
public Duration nextDelay(final Duration currentDelay) {
// current delay didn't exceed maxDelay already
if (currentDelay.compareTo(maxDelay) < 0) {
return calculateNextDelay(currentDelay);
}
return maxDelay;
}
/**
* Returns false if the RetryPolicy is configured with UNLIMITED_RETRIES
* or if the retriesCount param is lower than the maxRetries.
*
* @param retriesCount the current number of retries
* @return true if the number of retries reached the max number of retries, false otherwise
*/
public boolean reachedMaxRetries(final int retriesCount) {
return maxRetries != UNLIMITED_RETRIES && retriesCount >= maxRetries;
}
private Duration calculateNextDelay(final Duration currentDelay) {
val next = backoffFunction.apply(currentDelay);
if (next.compareTo(maxDelay) > 0) {
return maxDelay;
}
return next;
}
}

View File

@ -0,0 +1,66 @@
package io.spiffe.workloadapi.retry;
import java.time.Duration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Provides methods to schedule the execution of retries based on a backoff policy.
*/
public class RetryHandler {
private final ScheduledExecutorService executor;
private final ExponentialBackoffPolicy exponentialBackoffPolicy;
private Duration nextDelay;
private int retryCount;
public RetryHandler(final ExponentialBackoffPolicy exponentialBackoffPolicy, final ScheduledExecutorService executor) {
this.nextDelay = exponentialBackoffPolicy.getInitialDelay();
this.exponentialBackoffPolicy = exponentialBackoffPolicy;
this.executor = executor;
}
/**
* Schedule to execute a Runnable, based on the backoff policy
* Updates the next delay and retries count.
*
* @param runnable the task to be scheduled for execution
*/
public void scheduleRetry(final Runnable runnable) {
if (executor.isShutdown()) {
return;
}
if (exponentialBackoffPolicy.reachedMaxRetries(retryCount)) {
return;
}
executor.schedule(runnable, nextDelay.getSeconds(), TimeUnit.SECONDS);
nextDelay = exponentialBackoffPolicy.nextDelay(nextDelay);
retryCount++;
}
/**
* Returns true is a new retry should be performs, according the the Retry Policy.
* @return true is a new retry should be performs, according the the Retry Policy
*/
public boolean shouldRetry() {
return !exponentialBackoffPolicy.reachedMaxRetries(retryCount);
}
/**
* Reset state of RetryHandle to initial values.
*/
public void reset() {
nextDelay = exponentialBackoffPolicy.getInitialDelay();
retryCount = 0;
}
public Duration getNextDelay() {
return nextDelay;
}
public int getRetryCount() {
return retryCount;
}
}

View File

@ -0,0 +1,167 @@
syntax = "proto3";
option java_package = "io.spiffe.workloadapi.grpc";
import "google/protobuf/struct.proto";
service SpiffeWorkloadAPI {
/////////////////////////////////////////////////////////////////////////
// X509-SVID Profile
/////////////////////////////////////////////////////////////////////////
// Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to,
// as well as related information like trust bundles and CRLs. As this
// information changes, subsequent messages will be streamed from the
// server.
rpc FetchX509SVID(X509SVIDRequest) returns (stream X509SVIDResponse);
// Fetch trust bundles and CRLs. Useful for clients that only need to
// validate SVIDs without obtaining an SVID for themself. As this
// information changes, subsequent messages will be streamed from the
// server.
rpc FetchX509Bundles(X509BundlesRequest) returns (stream X509BundlesResponse);
/////////////////////////////////////////////////////////////////////////
// JWT-SVID Profile
/////////////////////////////////////////////////////////////////////////
// Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to,
// for the requested audience. If an optional SPIFFE ID is requested, only
// the JWT-SVID for that SPIFFE ID is returned.
rpc FetchJWTSVID(JWTSVIDRequest) returns (JWTSVIDResponse);
// Fetches the JWT bundles, formatted as JWKS documents, keyed by the
// SPIFFE ID of the trust domain. As this information changes, subsequent
// messages will be streamed from the server.
rpc FetchJWTBundles(JWTBundlesRequest) returns (stream JWTBundlesResponse);
// Validates a JWT-SVID against the requested audience. Returns the SPIFFE
// ID of the JWT-SVID and JWT claims.
rpc ValidateJWTSVID(ValidateJWTSVIDRequest) returns (ValidateJWTSVIDResponse);
}
// The X509SVIDRequest message conveys parameters for requesting an X.509-SVID.
// There are currently no request parameters.
message X509SVIDRequest { }
// The X509SVIDResponse message carries X.509-SVIDs and related information,
// including a set of global CRLs and a list of bundles the workload may use
// for federating with foreign trust domains.
message X509SVIDResponse {
// Required. A list of X509SVID messages, each of which includes a single
// X.509-SVID, its private key, and the bundle for the trust domain.
repeated X509SVID svids = 1;
// Optional. ASN.1 DER encoded certificate revocation lists.
repeated bytes crl = 2;
// Optional. CA certificate bundles belonging to foreign trust domains that
// the workload should trust, keyed by the SPIFFE ID of the foreign trust
// domain. Bundles are ASN.1 DER encoded.
map<string, bytes> federated_bundles = 3;
}
// The X509SVID message carries a single SVID and all associated information,
// including the X.509 bundle for the trust domain.
message X509SVID {
// Required. The SPIFFE ID of the SVID in this entry
string spiffe_id = 1;
// Required. ASN.1 DER encoded certificate chain. MAY include
// intermediates, the leaf certificate (or SVID itself) MUST come first.
bytes x509_svid = 2;
// Required. ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted.
bytes x509_svid_key = 3;
// Required. ASN.1 DER encoded X.509 bundle for the trust domain.
bytes bundle = 4;
// Optional. An operator-specified string used to provide guidance on how this
// identity should be used by a workload when more than one SVID is returned.
// For example, `internal` and `external` to indicate an SVID for internal or
// external use, respectively.
string hint = 5;
}
// The X509BundlesRequest message conveys parameters for requesting X.509
// bundles. There are currently no such parameters.
message X509BundlesRequest {
}
// The X509BundlesResponse message carries a set of global CRLs and a map of
// trust bundles the workload should trust.
message X509BundlesResponse {
// Optional. ASN.1 DER encoded certificate revocation lists.
repeated bytes crl = 1;
// Required. CA certificate bundles belonging to trust domains that the
// workload should trust, keyed by the SPIFFE ID of the trust domain.
// Bundles are ASN.1 DER encoded.
map<string, bytes> bundles = 2;
}
message JWTSVIDRequest {
// Required. The audience(s) the workload intends to authenticate against.
repeated string audience = 1;
// Optional. The requested SPIFFE ID for the JWT-SVID. If unset, all
// JWT-SVIDs to which the workload is entitled are requested.
string spiffe_id = 2;
}
// The JWTSVIDResponse message conveys JWT-SVIDs.
message JWTSVIDResponse {
// Required. The list of returned JWT-SVIDs.
repeated JWTSVID svids = 1;
}
// The JWTSVID message carries the JWT-SVID token and associated metadata.
message JWTSVID {
// Required. The SPIFFE ID of the JWT-SVID.
string spiffe_id = 1;
// Required. Encoded JWT using JWS Compact Serialization.
string svid = 2;
// Optional. An operator-specified string used to provide guidance on how this
// identity should be used by a workload when more than one SVID is returned.
// For example, `internal` and `external` to indicate an SVID for internal or
// external use, respectively.
string hint = 3;
}
// The JWTBundlesRequest message conveys parameters for requesting JWT bundles.
// There are currently no such parameters.
message JWTBundlesRequest { }
// The JWTBundlesReponse conveys JWT bundles.
message JWTBundlesResponse {
// Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust
// domain.
map<string, bytes> bundles = 1;
}
// The ValidateJWTSVIDRequest message conveys request parameters for
// JWT-SVID validation.
message ValidateJWTSVIDRequest {
// Required. The audience of the validating party. The JWT-SVID must
// contain an audience claim which contains this value in order to
// succesfully validate.
string audience = 1;
// Required. The JWT-SVID to validate, encoded using JWS Compact
// Serialization.
string svid = 2;
}
// The ValidateJWTSVIDReponse message conveys the JWT-SVID validation results.
message ValidateJWTSVIDResponse {
// Required. The SPIFFE ID of the validated JWT-SVID.
string spiffe_id = 1;
// Optional. Arbitrary claims contained within the payload of the validated
// JWT-SVID.
google.protobuf.Struct claims = 2;
}

View File

@ -0,0 +1,156 @@
package io.spiffe.bundle.jwtbundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.internal.DummyPublicKey;
import io.spiffe.spiffeid.TrustDomain;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class JwtBundleSetTest {
@Test
void testOfListOfBundles() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.parse("other.org"));
List<JwtBundle> bundles = Arrays.asList(jwtBundle1, jwtBundle2);
JwtBundleSet bundleSet = JwtBundleSet.of(bundles);
assertNotNull(bundleSet);
assertEquals(2, bundleSet.getBundles().size());
assertEquals(jwtBundle1, bundleSet.getBundles().get(TrustDomain.parse("example.org")));
assertEquals(jwtBundle2, bundleSet.getBundles().get(TrustDomain.parse("other.org")));
}
@Test
void getBundleForTrustDomain_Success() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.parse("other.org"));
List<JwtBundle> bundles = Arrays.asList(jwtBundle1, jwtBundle2);
JwtBundleSet bundleSet = JwtBundleSet.of(bundles);
JwtBundle bundle = null;
try {
bundle = bundleSet.getBundleForTrustDomain(TrustDomain.parse("example.org"));
} catch (BundleNotFoundException e) {
fail(e);
}
assertEquals(jwtBundle1, bundle);
}
@Test
void testOf_null_throwsNullPointerException() {
try {
JwtBundleSet.of(null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("bundles is marked non-null but is null", e.getMessage());
}
}
@Test
void testOf_EmptyCollection_throwsIllegalArgumentException() {
try {
JwtBundleSet.of(Collections.emptySet());
fail("should have thrown exception");
} catch (IllegalArgumentException e) {
assertEquals("JwtBundle collection is empty", e.getMessage());
}
}
@Test
void testEmptySet() {
JwtBundleSet jwtBundleSet = JwtBundleSet.emptySet();
assertNotNull(jwtBundleSet);
assertEquals(0, jwtBundleSet.getBundles().size());
}
@Test
void testgetBundleForTrustDomain_TrustDomainNotInSet_ThrowsBundleNotFoundException() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.parse("other.org"));
List<JwtBundle> bundles = Arrays.asList(jwtBundle1, jwtBundle2);
JwtBundleSet bundleSet = JwtBundleSet.of(bundles);
try {
bundleSet.getBundleForTrustDomain(TrustDomain.parse("domain.test"));
fail("exception expected");
} catch (BundleNotFoundException e) {
assertEquals("No JWT bundle for trust domain domain.test", e.getMessage());
}
}
@Test
void testgetBundleForTrustDomain_null_throwsNullPointerException() throws BundleNotFoundException {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
List<JwtBundle> bundleList = Collections.singletonList(jwtBundle1);
JwtBundleSet bundleSet = JwtBundleSet.of(bundleList);
try {
bundleSet.getBundleForTrustDomain(null);
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testAdd() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
List<JwtBundle> bundleList = Collections.singletonList(jwtBundle1);
JwtBundleSet bundleSet = JwtBundleSet.of(bundleList);
JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.parse("other.org"));
bundleSet.put(jwtBundle2);
assertTrue(bundleSet.getBundles().containsValue(jwtBundle1));
assertTrue(bundleSet.getBundles().containsValue(jwtBundle2));
}
@Test
void testAdd_sameBundleAgain_noDuplicate() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
List<JwtBundle> bundleList = Collections.singletonList(jwtBundle1);
JwtBundleSet bundleSet = JwtBundleSet.of(bundleList);
bundleSet.put(jwtBundle1);
assertEquals(1, bundleSet.getBundles().size());
assertTrue(bundleSet.getBundles().containsValue(jwtBundle1));
}
@Test
void testAdd_aDifferentBundleForSameTrustDomain_replacesWithNewBundle() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
List<JwtBundle> bundleList = Collections.singletonList(jwtBundle1);
JwtBundleSet bundleSet = JwtBundleSet.of(bundleList);
JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.parse("example.org"));
jwtBundle2.putJwtAuthority("key1", new DummyPublicKey());
bundleSet.put(jwtBundle2);
assertTrue(bundleSet.getBundles().containsValue(jwtBundle2));
assertFalse(bundleSet.getBundles().containsValue(jwtBundle1));
}
@Test
void add_null_throwsNullPointerException() {
JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.parse("example.org"));
List<JwtBundle> bundleList = Collections.singletonList(jwtBundle1);
JwtBundleSet bundleSet = JwtBundleSet.of(bundleList);
try {
bundleSet.put(null);
} catch (NullPointerException e) {
assertEquals("jwtBundle is marked non-null but is null", e.getMessage());
}
}
}

View File

@ -0,0 +1,371 @@
package io.spiffe.bundle.jwtbundle;
import com.nimbusds.jose.jwk.Curve;
import io.spiffe.exception.AuthorityNotFoundException;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtBundleException;
import io.spiffe.internal.DummyPublicKey;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.TestUtils;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.HashMap;
import static io.spiffe.utils.TestUtils.toUri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class JwtBundleTest {
@Test
void testNewJwtBundleWithTrustDomain_Success() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("example.org"), jwtBundle.getTrustDomain());
}
@Test
void testNewJwtBundleWithTrustDomainAndAuthorities_Success() {
HashMap<String, PublicKey> authorities = new HashMap<>();
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateRSAKeyPair(2048);
authorities.put("authority1", key1.getPublic());
authorities.put("authority2", key2.getPublic());
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"), authorities);
// change a key in the map, to test that the bundle has its own copy
authorities.put("authority1", key2.getPublic());
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("example.org"), jwtBundle.getTrustDomain());
assertEquals(2, jwtBundle.getJwtAuthorities().size());
assertEquals(key1.getPublic(), jwtBundle.getJwtAuthorities().get("authority1"));
assertEquals(key2.getPublic(), jwtBundle.getJwtAuthorities().get("authority2"));
}
@Test
void testNewJwtBundle_TrustDomainIsNull_ThrowsNullPointerException() {
try {
HashMap<String, PublicKey> authorities = new HashMap<>();
new JwtBundle(null, authorities);
fail("NullPointerException was expected");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testNewJwtBundleWithTrustDomain_AuthoritiesIsNull_ThrowsNullPointerException() {
try {
new JwtBundle(TrustDomain.parse("example.org"), null);
fail("NullPointerException was expected");
} catch (NullPointerException e) {
assertEquals("jwtAuthorities is marked non-null but is null", e.getMessage());
}
}
@Test
void testNewJwtBundleWithAuthorities_TrustDomainIsNull_ThrowsNullPointerException() {
try {
new JwtBundle(null);
fail("NullPointerException was expected");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testLoadFileWithEcKey_Success() throws URISyntaxException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_EC_1.json"));
TrustDomain trustDomain = TrustDomain.parse("example.org");
JwtBundle jwtBundle = null;
try {
jwtBundle = JwtBundle.load(trustDomain, path);
} catch (KeyException | JwtBundleException e) {
fail();
}
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("example.org"), jwtBundle.getTrustDomain());
assertEquals(1, jwtBundle.getJwtAuthorities().size());
assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD"));
}
@Test
void testLoadFileWithRsaKey_Success() throws URISyntaxException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_1.json"));
TrustDomain trustDomain = TrustDomain.parse("domain.test");
JwtBundle jwtBundle = null;
try {
jwtBundle = JwtBundle.load(trustDomain, path);
} catch (KeyException | JwtBundleException e) {
fail(e);
}
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("domain.test"), jwtBundle.getTrustDomain());
assertEquals(1, jwtBundle.getJwtAuthorities().size());
assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96"));
}
@Test
void testLoadFileWithRsaAndEc_Success() throws URISyntaxException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_EC.json"));
TrustDomain trustDomain = TrustDomain.parse("domain.test");
JwtBundle jwtBundle = null;
try {
jwtBundle = JwtBundle.load(trustDomain, path);
} catch (KeyException | JwtBundleException e) {
fail(e);
}
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("domain.test"), jwtBundle.getTrustDomain());
assertEquals(2, jwtBundle.getJwtAuthorities().size());
assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96"));
assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD"));
}
@Test
void testLoadFile_MissingKid_ThrowsJwtBundleException() throws URISyntaxException, KeyException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_missing_kid.json"));
TrustDomain trustDomain = TrustDomain.parse("domain.test");
try {
JwtBundle.load(trustDomain, path);
fail("should have thrown exception");
} catch (JwtBundleException e) {
assertEquals("Error adding authority of JWKS: keyID cannot be empty", e.getMessage());
}
}
@Test
void testLoadFile_InvalidKeyType_ThrowsKeyException() throws URISyntaxException, KeyException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_invalid_keytype.json"));
TrustDomain trustDomain = TrustDomain.parse("domain.test");
try {
JwtBundle.load(trustDomain, path);
fail("should have thrown exception");
} catch (JwtBundleException e) {
assertEquals("Unsupported JWT family algorithm: OKP", e.getCause().getMessage());
}
}
@Test
void testLoadFile_NonExistentFile_ThrowsException() throws KeyException {
Path path = Paths.get("testdata/jwtbundle/non-existen.json");
TrustDomain trustDomain = TrustDomain.parse("domain.test");
try {
JwtBundle.load(trustDomain, path);
fail("should have thrown exception");
} catch (JwtBundleException e) {
assertEquals("Could not load bundle from file: testdata/jwtbundle/non-existen.json", e.getMessage());
}
}
@Test
void testLoad_NullTrustDomain_ThrowsNullPointerException() throws KeyException, JwtBundleException {
try {
JwtBundle.load(null, Paths.get("path-to-file"));
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testLoad_NullBundlePath_ThrowsNullPointerException() throws KeyException, JwtBundleException {
try {
JwtBundle.load(TrustDomain.parse("example.org"), null);
} catch (NullPointerException e) {
assertEquals("bundlePath is marked non-null but is null", e.getMessage());
}
}
@Test
void testParseJsonWithRsaAndEcKeys_Success() throws URISyntaxException, IOException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_EC.json"));
byte[] bundleBytes = Files.readAllBytes(path);
JwtBundle jwtBundle = null;
try {
jwtBundle = JwtBundle.parse(TrustDomain.parse("domain.test"), bundleBytes);
} catch (JwtBundleException e) {
fail(e);
}
assertNotNull(jwtBundle);
assertEquals(TrustDomain.parse("domain.test"), jwtBundle.getTrustDomain());
assertEquals(2, jwtBundle.getJwtAuthorities().size());
assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96"));
assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD"));
}
@Test
void testParseJWKSWithEmptyKeysArray_Success() {
TrustDomain trustDomain = TrustDomain.parse("example.org");
String jwksEmptyKeysJson = "{\"keys\": []}";
byte[] bundleBytes = jwksEmptyKeysJson.getBytes(StandardCharsets.UTF_8);
JwtBundle jwtBundle = null;
try {
jwtBundle = JwtBundle.parse(trustDomain, bundleBytes);
} catch (JwtBundleException e) {
fail("Parsing failed with exception: " + e.getMessage());
}
assertNotNull(jwtBundle, "JwtBundle should not be null");
assertEquals(trustDomain, jwtBundle.getTrustDomain(), "Trust domain should match");
assertTrue(jwtBundle.getJwtAuthorities().isEmpty(), "JwtAuthorities should be empty");
}
@Test
void testParse_MissingKid_Fails() throws URISyntaxException, IOException {
Path path = Paths.get(toUri("testdata/jwtbundle/jwks_missing_kid.json"));
byte[] bundleBytes = Files.readAllBytes(path);
TrustDomain trustDomain = TrustDomain.parse("domain.test");
try {
JwtBundle.parse(trustDomain, bundleBytes);
fail("should have thrown exception");
} catch (JwtBundleException e) {
assertEquals("Error adding authority of JWKS: keyID cannot be empty", e.getMessage());
}
}
@Test
void testParseInvalidJson() throws KeyException {
try {
JwtBundle.parse(TrustDomain.parse("example.org"), "invalid json".getBytes());
fail("exception is expected");
} catch (JwtBundleException e) {
assertEquals("Could not parse bundle from bytes", e.getMessage());
}
}
@Test
void testParse_NullTrustDomain_ThrowsNullPointerException() throws KeyException, JwtBundleException {
try {
JwtBundle.parse(null, "json".getBytes());
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testParse_NullBundleBytes_ThrowsNullPointerException() throws KeyException, JwtBundleException {
try {
JwtBundle.parse(TrustDomain.parse("example.org"), null);
} catch (NullPointerException e) {
assertEquals("bundleBytes is marked non-null but is null", e.getMessage());
}
}
@Test
void testgetBundleForTrustDomain_Success() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
try {
JwtBundle bundle = jwtBundle.getBundleForTrustDomain(TrustDomain.parse("example.org"));
assertEquals(jwtBundle, bundle);
} catch (BundleNotFoundException e) {
fail(e);
}
}
@Test
void testgetBundleForTrustDomain_doesNotExiste_ThrowsBundleNotFoundException() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
try {
jwtBundle.getBundleForTrustDomain(TrustDomain.parse("other.org"));
fail("exception expected");
} catch (BundleNotFoundException e) {
assertEquals("No JWT bundle found for trust domain other.org", e.getMessage());
}
}
@Test
void testJWTAuthoritiesCRUD() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
// Test addJWTAuthority
DummyPublicKey jwtAuthority1 = new DummyPublicKey();
DummyPublicKey jwtAuthority2 = new DummyPublicKey();
jwtBundle.putJwtAuthority("key1", jwtAuthority1);
jwtBundle.putJwtAuthority("key2", jwtAuthority2);
assertEquals(2, jwtBundle.getJwtAuthorities().size());
// Test findJwtAuthority
PublicKey key1 = null;
PublicKey key2 = null;
try {
key1 = jwtBundle.findJwtAuthority("key1");
key2 = jwtBundle.findJwtAuthority("key2");
} catch (AuthorityNotFoundException e) {
fail(e);
}
assertEquals(key1, jwtAuthority1);
assertEquals(key2, jwtAuthority2);
// Test RemoveJwtAuthority
jwtBundle.removeJwtAuthority("key1");
assertFalse(jwtBundle.hasJwtAuthority("key1"));
assertTrue(jwtBundle.hasJwtAuthority("key2"));
// Test update
jwtBundle.putJwtAuthority("key2", jwtAuthority1);
assertEquals(jwtAuthority1, jwtBundle.getJwtAuthorities().get("key2"));
assertEquals(1, jwtBundle.getJwtAuthorities().size());
}
@Test
void testAddJwtAuthority_emtpyKeyId_throwsIllegalArgumentException() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
try {
jwtBundle.putJwtAuthority("", new DummyPublicKey());
} catch (IllegalArgumentException e) {
assertEquals("KeyId cannot be empty", e.getMessage());
}
}
@Test
void testAddJwtAuthority_nullKeyId_throwsNullPointerException() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
try {
jwtBundle.putJwtAuthority(null, new DummyPublicKey());
} catch (NullPointerException e) {
assertEquals("keyId is marked non-null but is null", e.getMessage());
}
}
@Test
void testAddJwtAuthority_nullJwtAuthority_throwsNullPointerException() {
JwtBundle jwtBundle = new JwtBundle(TrustDomain.parse("example.org"));
try {
jwtBundle.putJwtAuthority("key1", null);
} catch (NullPointerException e) {
assertEquals("jwtAuthority is marked non-null but is null", e.getMessage());
}
}
}

View File

@ -0,0 +1,152 @@
package io.spiffe.bundle.x509bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.internal.DummyX509Certificate;
import io.spiffe.spiffeid.TrustDomain;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class X509BundleSetTest {
@Test
void testOf_listOfBundles_Success() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("other.org"));
List<X509Bundle> bundleList = Arrays.asList(x509Bundle1, x509Bundle2);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
assertTrue(bundleSet.getBundles().containsValue(x509Bundle1));
assertTrue(bundleSet.getBundles().containsValue(x509Bundle2));
}
@Test
void testOf_null_throwsNullPointerException() {
try {
X509BundleSet.of(null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("bundles is marked non-null but is null", e.getMessage());
}
}
@Test
void testOf_emptyCollection_throwsIllegalArgumentException() {
try {
X509BundleSet.of(Collections.emptyList());
fail("should have thrown exception");
} catch (IllegalArgumentException e) {
assertEquals("X509Bundles collection is empty", e.getMessage());
}
}
@Test
void testEmptySet() {
X509BundleSet x509BundleSet = X509BundleSet.emptySet();
assertNotNull(x509BundleSet);
assertEquals(0, x509BundleSet.getBundles().size());
}
@Test
void testAdd() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
List<X509Bundle> bundleList = Collections.singletonList(x509Bundle1);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("other.org"));
bundleSet.put(x509Bundle2);
assertTrue(bundleSet.getBundles().containsValue(x509Bundle1));
assertTrue(bundleSet.getBundles().containsValue(x509Bundle2));
}
@Test
void testAdd_sameBundleAgain_noDuplicate() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
List<X509Bundle> bundleList = Collections.singletonList(x509Bundle1);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
bundleSet.put(x509Bundle1);
assertTrue(bundleSet.getBundles().containsValue(x509Bundle1));
assertEquals(1, bundleSet.getBundles().size());
}
@Test
void testAdd_aDifferentBundleForSameTrustDomain_replacesWithNewBundle() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
List<X509Bundle> bundleList = Collections.singletonList(x509Bundle1);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("example.org"));
x509Bundle2.addX509Authority(new DummyX509Certificate());
bundleSet.put(x509Bundle2);
assertTrue(bundleSet.getBundles().containsValue(x509Bundle2));
assertFalse(bundleSet.getBundles().containsValue(x509Bundle1));
assertEquals(1, bundleSet.getBundles().size());
}
@Test
void testAdd_nullBundle_throwsNullPointerException() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
List<X509Bundle> bundleList = Collections.singletonList(x509Bundle1);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
try {
bundleSet.put(null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("x509Bundle is marked non-null but is null", e.getMessage());
}
}
@Test
void testgetBundleForTrustDomain_Success() throws BundleNotFoundException {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("other.org"));
List<X509Bundle> bundleList = Arrays.asList(x509Bundle1, x509Bundle2);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
assertEquals(x509Bundle1, bundleSet.getBundleForTrustDomain(TrustDomain.parse("example.org")));
assertEquals(x509Bundle2, bundleSet.getBundleForTrustDomain(TrustDomain.parse("other.org")));
}
@Test
void testgetBundleForTrustDomain_notFoundTrustDomain() {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("other.org"));
List<X509Bundle> bundleList = Arrays.asList(x509Bundle1, x509Bundle2);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
try {
bundleSet.getBundleForTrustDomain(TrustDomain.parse("unknown.org"));
fail("expected BundleNotFoundException");
} catch (BundleNotFoundException e) {
assertEquals("No X.509 bundle for trust domain unknown.org", e.getMessage());
}
}
@Test
void testgetBundleForTrustDomain_nullTrustDomain_throwsException() throws BundleNotFoundException {
X509Bundle x509Bundle1 = new X509Bundle(TrustDomain.parse("example.org"));
X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.parse("other.org"));
List<X509Bundle> bundleList = Arrays.asList(x509Bundle1, x509Bundle2);
X509BundleSet bundleSet = X509BundleSet.of(bundleList);
try {
bundleSet.getBundleForTrustDomain(null);
fail("expected exception");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
}

View File

@ -0,0 +1,344 @@
package io.spiffe.bundle.x509bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.X509BundleException;
import io.spiffe.internal.DummyX509Certificate;
import io.spiffe.spiffeid.TrustDomain;
import lombok.Builder;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.util.StringUtils;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.stream.Stream;
import static io.spiffe.utils.TestUtils.toUri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class X509BundleTest {
@Test
void TestNewBundle() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
assertEquals(0, x509Bundle.getX509Authorities().size());
assertEquals(TrustDomain.parse("example.org"), x509Bundle.getTrustDomain());
}
@Test
void testNewBundle_nullTrustDomain_throwsNullPointerException() {
try {
new X509Bundle(null );
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testNewBundleWithAuthorities_nullTrustDomain_throwsNullPointerException() {
try {
new X509Bundle(null, new HashSet<>());
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testNewBundleAuthorities_nullAuthorities_throwsNullPointerException() {
try {
new X509Bundle(TrustDomain.parse("example.org"), null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("x509Authorities is marked non-null but is null", e.getMessage());
}
}
@Test
void TestFromAuthorities() {
X509Certificate x509Cert1 = new DummyX509Certificate();
X509Certificate x509Cert2 = new DummyX509Certificate();
HashSet<X509Certificate> authorities = new HashSet<>();
authorities.add(x509Cert1);
authorities.add(x509Cert2);
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"), authorities);
assertEquals(authorities, x509Bundle.getX509Authorities());
}
@Test
void testGetBundleForTrustDomain() throws BundleNotFoundException {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
assertEquals(x509Bundle, x509Bundle.getBundleForTrustDomain(TrustDomain.parse("example.org")));
}
@Test
void testGetBundleForTrustDomain_notBundleFound_throwsBundleNotFoundException() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
try {
x509Bundle.getBundleForTrustDomain(TrustDomain.parse("other.org"));
} catch (BundleNotFoundException e) {
assertEquals("No X.509 bundle found for trust domain other.org", e.getMessage());
}
}
@Test
void testGetBundleForTrustDomain_nullArgument_throwsNullPointerException() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
try {
x509Bundle.getBundleForTrustDomain(null);
fail();
} catch (BundleNotFoundException e) {
fail();
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void TestLoad_Succeeds() {
try {
X509Bundle x509Bundle = X509Bundle.load(TrustDomain.parse("example.org"), Paths.get(toUri("testdata/x509bundle/certs.pem")));
assertEquals(2, x509Bundle.getX509Authorities().size());
} catch (URISyntaxException | X509BundleException e) {
fail(e);
}
}
@Test
void TestLoad_Fails() {
try {
X509Bundle.load(TrustDomain.parse("example.org"), Paths.get("testdata/x509bundle/non-existent.pem"));
fail("should have thrown exception");
} catch (X509BundleException e) {
assertEquals("Unable to load X.509 bundle file", e.getMessage());
}
}
@Test
void testLoad_nullTrustDomain_throwsNullPointerException() throws X509BundleException {
try {
X509Bundle.load(null,Paths.get("testdata/x509bundle/non-existent.pem"));
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testLoad_nullBundlePath_throwsNullPointerException() throws X509BundleException {
try {
X509Bundle.load(TrustDomain.parse("example.org"), null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("bundlePath is marked non-null but is null", e.getMessage());
}
}
@Test
void testParse_nullTrustDomain_throwsNullPointerException() throws X509BundleException {
try {
X509Bundle.parse(null, "bytes".getBytes());
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void testParse_nullBundlePath_throwsNullPointerException() throws X509BundleException {
try {
X509Bundle.parse(TrustDomain.parse("example.org"), null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("bundleBytes is marked non-null but is null", e.getMessage());
}
}
@Test
void testHasAuthority_nullArgument_throwsNullPointerException() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
try {
x509Bundle.hasX509Authority(null);
fail();
} catch (NullPointerException e) {
assertEquals("x509Authority is marked non-null but is null", e.getMessage());
}
}
@Test
void testAddAuthority_nullArgument_throwsNullPointerException() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
try {
x509Bundle.addX509Authority(null);
fail();
} catch (NullPointerException e) {
assertEquals("x509Authority is marked non-null but is null", e.getMessage());
}
}
@Test
void testRemoveAuthority_nullArgument_throwsNullPointerException() {
X509Bundle x509Bundle = new X509Bundle(TrustDomain.parse("example.org"));
try {
x509Bundle.removeX509Authority(null);
fail();
} catch (NullPointerException e) {
assertEquals("x509Authority is marked non-null but is null", e.getMessage());
}
}
@Test
void TestX509AuthoritiesCRUD() {
X509Bundle bundle1 = null;
X509Bundle bundle2 = null;
try {
// Load bundle1, which contains a single certificate
bundle1 = X509Bundle.load(TrustDomain.parse("example.org"), Paths.get(toUri("testdata/x509bundle/cert.pem")));
// Load bundle2, which contains 2 certificates
// The first certificate is the same than the one used in bundle1
bundle2 = X509Bundle.load(TrustDomain.parse("example.org"), Paths.get(toUri("testdata/x509bundle/certs.pem")));
} catch (URISyntaxException | X509BundleException e) {
fail(e);
}
assertEquals(1, bundle1.getX509Authorities().size());
assertEquals(2, bundle2.getX509Authorities().size());
assertTrue(bundle2.hasX509Authority((X509Certificate) bundle1.getX509Authorities().toArray()[0]));
// Adding a new authority increases the x509Authorities slice length
bundle1.addX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[1]);
assertEquals(2, bundle1.getX509Authorities().size());
assertTrue(bundle1.hasX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[0]));
assertTrue(bundle1.hasX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[1]));
// If the authority already exist, it should not be added again
bundle1.addX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[0]);
bundle1.addX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[1]);
assertEquals(2, bundle1.getX509Authorities().size());
assertTrue(bundle1.hasX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[0]));
assertTrue(bundle1.hasX509Authority((X509Certificate) bundle2.getX509Authorities().toArray()[1]));
// Removing an authority, decreases the authority slice length
X509Certificate cert = (X509Certificate) bundle1.getX509Authorities().toArray()[0];
bundle1.removeX509Authority(cert);
assertEquals(1, bundle1.getX509Authorities().size());
assertFalse(bundle1.hasX509Authority(cert));
// If the authority does not exist, it should keep its size
bundle1.removeX509Authority(cert);
assertEquals(1, bundle1.getX509Authorities().size());
assertFalse(bundle1.hasX509Authority(cert));
}
@ParameterizedTest
@MethodSource("provideX509BundleScenarios")
void parseX509Bundle(TestCase testCase) {
try {
Path path = Paths.get(toUri(testCase.path));
byte[] bytes = Files.readAllBytes(path);
X509Bundle x509Bundle = X509Bundle.parse(testCase.trustDomain, bytes);
if (StringUtils.isNotBlank(testCase.expectedError)) {
fail(String.format("Error was expected: %s", testCase.expectedError));
}
assertNotNull(x509Bundle);
assertEquals(testCase.trustDomain, x509Bundle.getTrustDomain());
assertEquals(testCase.expectedNumberOfAuthorities, x509Bundle.getX509Authorities().size());
} catch (Exception e) {
if (StringUtils.isBlank(testCase.expectedError)) {
fail(e);
}
assertEquals(testCase.expectedError, e.getMessage());
}
}
static Stream<Arguments> provideX509BundleScenarios() {
return Stream.of(
Arguments.of(TestCase
.builder()
.name("Parse multiple certificates should succeed")
.path("testdata/x509bundle/certs.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedNumberOfAuthorities(2)
.build()
),
Arguments.of(TestCase
.builder()
.name("Parse single certificate should succeed")
.path("testdata/x509bundle/cert.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedNumberOfAuthorities(1)
.build()
),
Arguments.of(TestCase
.builder()
.name("Parse empty bytes should fail")
.path("testdata/x509bundle/empty.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedError("Bundle certificates could not be parsed from bundle path")
.build()
),
Arguments.of(TestCase
.builder()
.name("Parse non-PEM bytes should fail")
.path("testdata/x509bundle/not-pem.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedError("Bundle certificates could not be parsed from bundle path")
.build()
),
Arguments.of(TestCase
.builder()
.name("Parse should fail if no certificate block is is found")
.path("testdata/x509bundle/key.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedError("Bundle certificates could not be parsed from bundle path")
.build()
),
Arguments.of(TestCase
.builder()
.name("Parse a corrupted certificate should fail")
.path("testdata/x509bundle/corrupted.pem")
.trustDomain(TrustDomain.parse("example.org"))
.expectedError("Bundle certificates could not be parsed from bundle path")
.build()
)
);
}
@Value
static class TestCase {
String name;
TrustDomain trustDomain;
String path;
int expectedNumberOfAuthorities;
String expectedError;
@Builder
public TestCase(String name, TrustDomain trustDomain, String path, int expectedNumberOfAuthorities, String expectedError) {
this.name = name;
this.trustDomain = trustDomain;
this.path = path;
this.expectedNumberOfAuthorities = expectedNumberOfAuthorities;
this.expectedError = expectedError;
}
}
}

View File

@ -0,0 +1,29 @@
package io.spiffe.internal;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AsymmetricKeyAlgorithmTest {
@Test
void parseRSA() {
AsymmetricKeyAlgorithm algorithm = AsymmetricKeyAlgorithm.parse("RSA");
assertEquals(AsymmetricKeyAlgorithm.RSA, algorithm);
}
@Test
void parseEC() {
AsymmetricKeyAlgorithm algorithm = AsymmetricKeyAlgorithm.parse("EC");
assertEquals(AsymmetricKeyAlgorithm.EC, algorithm);
}
@Test
void parseUnknown() {
try {
AsymmetricKeyAlgorithm.parse("unknown");
} catch (IllegalArgumentException e) {
assertEquals("Algorithm not supported: unknown", e.getMessage());
}
}
}

View File

@ -0,0 +1,197 @@
package io.spiffe.internal;
import com.nimbusds.jose.jwk.Curve;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.TestUtils;
import lombok.val;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA;
import static io.spiffe.utils.TestUtils.toUri;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificate;
import static io.spiffe.utils.X509CertificateTestUtils.createRootCA;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
public class CertificateUtilsTest {
@Test
void generateCertificates_ofPEMByteArray_returnsListWithOneX509Certificate() throws IOException, URISyntaxException {
val path = Paths.get(toUri("testdata/internal/cert.pem"));
val certBytes = Files.readAllBytes(path);
List<X509Certificate> x509CertificateList;
SpiffeId spiffeId = null;
try {
x509CertificateList = CertificateUtils.generateCertificates(certBytes);
spiffeId = CertificateUtils.getSpiffeId(x509CertificateList.get(0));
} catch (CertificateException e) {
fail("Not expected exception. Should have generated the certificates", e);
}
assertEquals("spiffe://example.org/test", spiffeId.toString());
}
@Test
void validate_certificateThatIsExpired_throwsCertificateException() throws IOException, CertificateException, URISyntaxException {
val certPath = Paths.get(toUri("testdata/internal/cert2.pem"));
val certBundle = Paths.get(toUri("testdata/internal/bundle.pem"));
val certBytes = Files.readAllBytes(certPath);
val bundleBytes = Files.readAllBytes(certBundle);
val chain = CertificateUtils.generateCertificates(certBytes);
val trustedCert = CertificateUtils.generateCertificates(bundleBytes);
try {
CertificateUtils.validate(chain, trustedCert);
fail("Expected exception");
} catch (CertPathValidatorException e) {
assertEquals("validity check failed", e.getMessage());
}
}
@Test
void validateCerts_nullTrustedCerts() throws URISyntaxException, IOException, CertificateParsingException {
val certPath = Paths.get(toUri("testdata/internal/cert2.pem"));
val certBytes = Files.readAllBytes(certPath);
val chain = CertificateUtils.generateCertificates(certBytes);
try {
CertificateUtils.validate(chain, null);
} catch (CertificateException e) {
assertEquals("No trusted Certs", e.getMessage());
} catch (CertPathValidatorException e) {
fail(e);
}
}
@Test
void validateCerts_emptyTrustedCerts() throws URISyntaxException, IOException, CertificateParsingException {
val certPath = Paths.get(toUri("testdata/internal/cert2.pem"));
val certBytes = Files.readAllBytes(certPath);
val chain = CertificateUtils.generateCertificates(certBytes);
try {
CertificateUtils.validate(chain, Collections.emptyList());
} catch (CertificateException e) {
assertEquals("No trusted Certs", e.getMessage());
} catch (CertPathValidatorException e) {
fail(e);
}
}
@Test
void validateCerts_nullChain() throws URISyntaxException, IOException, CertificateParsingException {
val certPath = Paths.get(toUri("testdata/internal/cert2.pem"));
val certBytes = Files.readAllBytes(certPath);
val certificates = CertificateUtils.generateCertificates(certBytes);
try {
CertificateUtils.validate(null, certificates);
} catch (CertificateException | CertPathValidatorException e) {
fail(e);
} catch (IllegalArgumentException e) {
assertEquals("Chain of certificates is empty", e.getMessage());
}
}
@Test
void validateCerts_emptyChain() throws URISyntaxException, IOException, CertificateParsingException {
val certPath = Paths.get(toUri("testdata/internal/cert2.pem"));
val certBytes = Files.readAllBytes(certPath);
val certificates = CertificateUtils.generateCertificates(certBytes);
try {
CertificateUtils.validate(Collections.emptyList(), certificates);
} catch (CertificateException | CertPathValidatorException e) {
fail(e);
} catch (IllegalArgumentException e) {
assertEquals("Chain of certificates is empty", e.getMessage());
}
}
@Test
void testGenerateRsaPrivateKeyFromBytes() throws URISyntaxException, IOException {
val keyPath = Paths.get(toUri("testdata/internal/privateKeyRsa.pem"));
val keyBytes = Files.readAllBytes(keyPath);
try {
PrivateKey privateKey = CertificateUtils.generatePrivateKey(keyBytes, RSA, KeyFileFormat.PEM);
assertNotNull(privateKey);
assertEquals(RSA.value(), privateKey.getAlgorithm());
} catch (InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException e) {
fail("Should have generated key", e);
}
}
@Test
void testGenerateEcPrivateKeyFromBytes() {
KeyPair ecKeyPair = TestUtils.generateECKeyPair(Curve.P_256);
byte[] keyBytes = ecKeyPair.getPrivate().getEncoded();
try {
PrivateKey privateKey = CertificateUtils.generatePrivateKey(keyBytes, AsymmetricKeyAlgorithm.EC, KeyFileFormat.DER);
assertNotNull(privateKey);
assertEquals("EC", privateKey.getAlgorithm());
} catch (InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException e) {
fail("Should have generated key", e);
}
}
@Test
void testGetSpiffeId() throws Exception {
val rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://domain.test" );
val leaf = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://domain.test/workload", rootCa, false);
SpiffeId spiffeId = CertificateUtils.getSpiffeId(leaf.getCertificate());
assertEquals(SpiffeId.parse("spiffe://domain.test/workload"), spiffeId);
}
@Test
void testGetSpiffeId_certNotContainSpiffeId_throwsCertificateException() throws Exception {
val rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://domain.test" );
val leaf = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "", rootCa, false);
try {
CertificateUtils.getSpiffeId(leaf.getCertificate());
fail("exception is expected");
} catch (CertificateException e) {
assertEquals("Certificate does not contain SPIFFE ID in the URI SAN", e.getMessage());
}
}
@Test
void testGetTrustDomain() throws Exception {
val rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://domain.test" );
val intermediate = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://domain.test/host", rootCa, true);
val leaf = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://domain.test/workload", intermediate, false);
val chain = Arrays.asList(leaf.getCertificate(), intermediate.getCertificate());
try {
TrustDomain trustDomain = CertificateUtils.getTrustDomain(chain);
assertNotNull(trustDomain);
assertEquals(TrustDomain.parse("domain.test"), trustDomain);
} catch (CertificateException e) {
fail(e);
}
}
}

View File

@ -0,0 +1,20 @@
package io.spiffe.internal;
import java.security.PublicKey;
public class DummyPublicKey implements PublicKey {
@Override
public String getAlgorithm() {
return null;
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return new byte[0];
}
}

View File

@ -0,0 +1,149 @@
package io.spiffe.internal;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Set;
// Dummy implementation of an X509Certificate used for testing purposes
public class DummyX509Certificate extends X509Certificate {
@Override
public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException {
}
@Override
public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException {
}
@Override
public int getVersion() {
return 0;
}
@Override
public BigInteger getSerialNumber() {
return null;
}
@Override
public Principal getIssuerDN() {
return null;
}
@Override
public Principal getSubjectDN() {
return null;
}
@Override
public Date getNotBefore() {
return null;
}
@Override
public Date getNotAfter() {
return null;
}
@Override
public byte[] getTBSCertificate() throws CertificateEncodingException {
return new byte[0];
}
@Override
public byte[] getSignature() {
return new byte[0];
}
@Override
public String getSigAlgName() {
return null;
}
@Override
public String getSigAlgOID() {
return null;
}
@Override
public byte[] getSigAlgParams() {
return new byte[0];
}
@Override
public boolean[] getIssuerUniqueID() {
return new boolean[0];
}
@Override
public boolean[] getSubjectUniqueID() {
return new boolean[0];
}
@Override
public boolean[] getKeyUsage() {
return new boolean[0];
}
@Override
public int getBasicConstraints() {
return 0;
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return new byte[0];
}
@Override
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
}
@Override
public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
}
@Override
public String toString() {
return null;
}
@Override
public PublicKey getPublicKey() {
return null;
}
@Override
public boolean hasUnsupportedCriticalExtension() {
return false;
}
@Override
public Set<String> getCriticalExtensionOIDs() {
return null;
}
@Override
public Set<String> getNonCriticalExtensionOIDs() {
return null;
}
@Override
public byte[] getExtensionValue(String oid) {
return new byte[0];
}
}

View File

@ -0,0 +1,236 @@
package io.spiffe.spiffeid;
import com.google.common.collect.Sets;
import io.spiffe.exception.InvalidSpiffeIdException;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class SpiffeIdTest {
private static final Set<Character> LOWER_ALPHA = Sets.newHashSet('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
private static final Set<Character> UPPER_ALPHA = Sets.newHashSet('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z');
private static final Set<Character> NUMBERS = Sets.newHashSet('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
private static final Set<Character> SPECIAL_CHARS = Sets.newHashSet('.', '-', '_');
static final Set<Character> TD_CHARS = Stream.of(
LOWER_ALPHA,
NUMBERS,
SPECIAL_CHARS)
.flatMap(Set::stream)
.collect(Collectors.toSet());
static final Set<Character> PATH_CHARS = Stream.of(
LOWER_ALPHA,
UPPER_ALPHA,
NUMBERS,
SPECIAL_CHARS)
.flatMap(Set::stream)
.collect(Collectors.toSet());
@Test
void toString_SpiffeId_ReturnsTheSpiffeIdInAStringFormatIncludingTheSchema() {
val trustDomain = TrustDomain.parse("trustdomain");
val spiffeId = SpiffeId.fromSegments(trustDomain, "path1", "path2", "path3");
assertEquals("spiffe://trustdomain/path1/path2/path3", spiffeId.toString());
}
@ParameterizedTest
@MethodSource("provideTestValidSpiffeIds")
void testParseValidSpiffeId(String input, TrustDomain expectedTrustDomain, String expectedPath) {
SpiffeId result;
try {
result = SpiffeId.parse(input);
assertEquals(expectedTrustDomain, result.getTrustDomain());
assertEquals(expectedPath, result.getPath());
} catch (Exception e) {
fail("Unexpected error", e);
}
}
static Stream<Arguments> provideTestValidSpiffeIds() {
return Stream.of(
Arguments.of("spiffe://trustdomain/path", TrustDomain.parse("trustdomain"), "/path"),
Arguments.of("spiffe://trustdomain/path1/path2", TrustDomain.parse("trustdomain"), "/path1/path2"),
Arguments.of("spiffe://trustdomain/PATH1/PATH2", TrustDomain.parse("trustdomain"), "/PATH1/PATH2"),
Arguments.of("spiffe://trustdomain/9eebccd2-12bf-40a6-b262-65fe0487d453", TrustDomain.parse("trustdomain"), "/9eebccd2-12bf-40a6-b262-65fe0487d453")
);
}
@ParameterizedTest
@MethodSource("provideInvalidSpiffeIds")
void testParseInvalidSpiffeId(String input, String expected) {
try {
SpiffeId.parse(input);
fail("Expected validation SPIFFE ID error");
} catch (Exception e) {
assertEquals(expected, e.getMessage());
}
}
static Stream<Arguments> provideInvalidSpiffeIds() {
return Stream.of(
Arguments.of("", "Cannot be empty"),
Arguments.of(null, "Cannot be empty"),
Arguments.of("192.168.2.2:6688", "Scheme is missing or invalid"),
Arguments.of("http://domain.test/path/element", "Scheme is missing or invalid"),
Arguments.of("spiffe:///path/element", "Trust domain is missing"),
Arguments.of("spiffe://domain.test/path/element?query=1", "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://domain.test/path/element?#fragment-1", "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://domain.test:8080/path/element", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://user:password@test.org/path/element", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe:path/element", "Scheme is missing or invalid"),
Arguments.of("spiffe:/path/element", "Scheme is missing or invalid"),
Arguments.of("SPIFFE://path/element", "Scheme is missing or invalid"),
Arguments.of("spiffe://domain.test/path/elem%5uent", "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://trustdomain/path//", "Path cannot contain empty segments"),
Arguments.of("spiffe://trustdomain/./other", "Path cannot contain dot segments"),
Arguments.of("spiffe://trustdomain/../other", "Path cannot contain dot segments"),
Arguments.of("spiffe://trustdomain/", "Path cannot have a trailing slash"),
Arguments.of("spiffe://trustdomain/path/", "Path cannot have a trailing slash")
);
}
@ParameterizedTest
@MethodSource("provideValidTrustDomainAndPaths")
void testOf(TrustDomain inputTrustDomain, String[] inputPath, SpiffeId expectedSpiffeId) {
try {
SpiffeId result = SpiffeId.fromSegments(inputTrustDomain, inputPath);
assertEquals(expectedSpiffeId, result);
} catch (Exception e) {
fail("Unexpected error", e);
}
}
static Stream<Arguments> provideValidTrustDomainAndPaths() {
return Stream.of(
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"path"}, SpiffeId.parse("spiffe://trustdomain/path")),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"path1", "path2"}, SpiffeId.parse("spiffe://trustdomain/path1/path2")),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"PATH1", "PATH2"}, SpiffeId.parse("spiffe://trustdomain/PATH1/PATH2")),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"path1", "path2"}, SpiffeId.parse("spiffe://trustdomain/path1/path2")),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"9eebccd2-12bf-40a6-b262-65fe0487d453"}, SpiffeId.parse("spiffe://trustdomain/9eebccd2-12bf-40a6-b262-65fe0487d453"))
);
}
@ParameterizedTest
@MethodSource("provideInvalidArguments")
void testOfInvalid(TrustDomain trustDomain, String[] inputPath, String expectedError) {
try {
SpiffeId.fromSegments(trustDomain, inputPath);
fail(String.format("Expected error %s", expectedError));
} catch (Exception e) {
assertEquals(expectedError, e.getMessage());
}
}
static Stream<Arguments> provideInvalidArguments() {
return Stream.of(
Arguments.of(null, new String[]{""}, "trustDomain is marked non-null but is null"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/ele%5ment"}, "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/path/"}, "Path cannot have a trailing slash"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/ /"}, "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/"}, "Path cannot have a trailing slash"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"//"}, "Path cannot contain empty segments"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/./"}, "Path cannot contain dot segments"),
Arguments.of(TrustDomain.parse("trustdomain"), new String[]{"/../"}, "Path cannot contain dot segments")
);
}
@Test
void testParseWithAllChars() {
// Go all the way through 255, which ensures we reject UTF-8 appropriately
for (int i = 0; i < 256; i++) {
char c = (char) i;
// Don't test '/' since it is the delimiter between path segments
if (c == '/') {
continue;
}
String path = "/path" + c;
if (PATH_CHARS.contains(c)) {
SpiffeId spiffeId = SpiffeId.parse("spiffe://trustdomain" + path);
assertEquals(spiffeId.toString(), "spiffe://trustdomain" + path);
} else {
try {
SpiffeId.parse("spiffe://trustdomain" + path);
} catch (InvalidSpiffeIdException e) {
assertEquals("Path segment characters are limited to letters, numbers, dots, dashes, and underscores", e.getMessage());
}
}
String td = "spiffe://trustdomain" + c;
if (TD_CHARS.contains(c)) {
SpiffeId spiffeId = SpiffeId.parse(td);
assertEquals(spiffeId.toString(), td);
} else {
try {
SpiffeId.parse(td);
} catch (InvalidSpiffeIdException e) {
assertEquals("Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores", e.getMessage());
}
}
}
}
@Test
void testOfWithAllChars() {
// Go all the way through 255, which ensures we reject UTF-8 appropriately
for (int i = 0; i < 256; i++) {
char c = (char) i;
// Don't test '/' since it is the delimiter between path segments
if (c == '/') {
continue;
}
String path1 = "path1" + c;
String path2 = "path2" + c;
TrustDomain td = TrustDomain.parse("trustdomain");
if (PATH_CHARS.contains(c)) {
SpiffeId spiffeId = SpiffeId.fromSegments(td, path1, path2);
assertEquals(spiffeId.toString(), String.format("spiffe://trustdomain/%s/%s", path1, path2));
} else {
try {
SpiffeId.fromSegments(td, path1, path2);
} catch (InvalidSpiffeIdException e) {
assertEquals("Path segment characters are limited to letters, numbers, dots, dashes, and underscores", e.getMessage());
}
}
}
}
@Test
void memberOf_aTrustDomainAndASpiffeIdWithSameTrustDomain_ReturnsTrue() {
val trustDomain = TrustDomain.parse("trustdomain");
val spiffeId = SpiffeId.fromSegments(trustDomain, "path1", "path2");
val isMemberOf = spiffeId.memberOf(TrustDomain.parse("trustdomain"));
assertTrue(isMemberOf);
}
@Test
void memberOf_aTrustDomainAndASpiffeIdWithDifferentTrustDomain_ReturnsFalse() {
val trustDomain = TrustDomain.parse("trustdomain");
val spiffeId = SpiffeId.fromSegments(trustDomain, "path1", "path2");
val isMemberOf = spiffeId.memberOf(TrustDomain.parse("otherdomain"));
assertFalse(isMemberOf);
}
}

View File

@ -0,0 +1,107 @@
package io.spiffe.spiffeid;
import lombok.val;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Set;
import static io.spiffe.utils.TestUtils.toUri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class SpiffeIdUtilsTest {
@Test
void getSpiffeIdSetFromFile() throws URISyntaxException {
Path path = Paths.get(toUri("testdata/spiffeid/spiffeIds.txt"));
try {
Set<SpiffeId> spiffeIdSet = SpiffeIdUtils.getSpiffeIdSetFromFile(path);
assertNotNull(spiffeIdSet);
assertEquals(3, spiffeIdSet.size());
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload1")));
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload2")));
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example2.org/workload1")));
} catch (IOException e) {
fail(e);
}
}
@Test
void getSpiffeIdSetFromNonExistenFile_throwsException() throws IOException {
Path path = Paths.get("testdata/spiffeid/non-existent-file");
try {
SpiffeIdUtils.getSpiffeIdSetFromFile(path);
fail("should have thrown exception");
} catch (NoSuchFileException e) {
assertEquals("testdata/spiffeid/non-existent-file", e.getMessage());
}
}
@Test
void toSetOfSpiffeIdsDefaultSeparator() {
val spiffeIdsAsString = "spiffe://example.org/workload1|spiffe://example.org/workload2";
val spiffeIdSet = SpiffeIdUtils.toSetOfSpiffeIds(spiffeIdsAsString);
assertNotNull(spiffeIdSet);
assertEquals(2, spiffeIdSet.size());
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload1")));
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload2")));
}
@Test
void toSetOfSpiffeIdsBlankSpaceSeparator() {
val spiffeIdsAsString = "spiffe://example.org/workload1 spiffe://example.org/workload2";
val spiffeIdSet = SpiffeIdUtils.toSetOfSpiffeIds(spiffeIdsAsString, ' ');
assertNotNull(spiffeIdSet);
assertEquals(2, spiffeIdSet.size());
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload1")));
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload2")));
}
@Test
void toSetOfSpiffeIdsCommaSeparator() {
val spiffeIdsAsString = "spiffe://example.org/workload1,spiffe://example.org/workload2";
val spiffeIdSet = SpiffeIdUtils.toSetOfSpiffeIds(spiffeIdsAsString, ',');
assertNotNull(spiffeIdSet);
assertEquals(2, spiffeIdSet.size());
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload1")));
assertTrue(spiffeIdSet.contains(SpiffeId.parse("spiffe://example.org/workload2")));
}
@Test
void toSetOfSpiffeIdsNullString() {
Set<SpiffeId> result = SpiffeIdUtils.toSetOfSpiffeIds(null);
assertEquals(Collections.emptySet(), result);
}
@Test
void toSetOfSpiffeIdsBlankString() {
Set<SpiffeId> result = SpiffeIdUtils.toSetOfSpiffeIds("");
assertEquals(Collections.emptySet(), result);
}
@Test
void testPrivateConstructor_InstanceCannotBeCreated() throws IllegalAccessException, InstantiationException {
val constructor = SpiffeIdUtils.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
try {
constructor.newInstance();
fail();
} catch (InvocationTargetException e) {
assertEquals("This is a utility class and cannot be instantiated", e.getCause().getMessage());
}
}
}

View File

@ -0,0 +1,108 @@
package io.spiffe.spiffeid;
import io.spiffe.exception.InvalidSpiffeIdException;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static io.spiffe.spiffeid.SpiffeIdTest.TD_CHARS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class TrustDomainTest {
@Test
void testTrustDomainFromName() {
TrustDomain trustDomain = TrustDomain.parse("trustdomain");
assertEquals("trustdomain", trustDomain.getName());
}
@Test
void testFromIdStringWithoutPath() {
TrustDomain trustDomain = TrustDomain.parse("spiffe://trustdomain");
assertEquals("trustdomain", trustDomain.getName());
}
@Test
void testFromIdStringWithPath() {
TrustDomain trustDomain = TrustDomain.parse("spiffe://trustdomain/path");
assertEquals("trustdomain", trustDomain.getName());
}
@Test
void testAllChars() {
// Go all the way through 255, which ensures we reject UTF-8 appropriately
for (int i = 0; i < 256; i++) {
char c = (char) i;
String td = "trustdomain" + c;
if (TD_CHARS.contains(c)) {
TrustDomain trustDomain = TrustDomain.parse(td);
assertEquals(td, trustDomain.getName());
} else {
try {
TrustDomain.parse(td);
} catch (InvalidSpiffeIdException e) {
assertEquals(e.getMessage(), "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores");
}
}
}
}
@ParameterizedTest
@MethodSource("provideInvalidTrustDomain")
void testParseTrustDomain(String input, Object expected) {
try {
TrustDomain.parse(input);
fail("error expected");
} catch (Exception e) {
assertEquals(expected, e.getMessage().trim());
}
}
static Stream<Arguments> provideInvalidTrustDomain() {
return Stream.of(
Arguments.of("", "Trust domain is missing"),
Arguments.of("spiffe://", "Trust domain is missing"),
Arguments.of(null, "idOrName is marked non-null but is null"),
Arguments.of("Trustdomain", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://Domain.test", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://domain.test/spiffe://domain.test/path/element", "Path segment characters are limited to letters, numbers, dots, dashes, and underscores"),
Arguments.of("http://domain.test", "Scheme is missing or invalid"),
Arguments.of("spiffe:// domain.test ", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("://domain.test", "Scheme is missing or invalid"),
Arguments.of("spiffe:///path/element", "Trust domain is missing"),
Arguments.of("/path/element", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe://domain.test:80", "Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores"),
Arguments.of("spiffe:/trustdomain/path", "Scheme is missing or invalid"),
Arguments.of("spiffe://trustdomain/", "Path cannot have a trailing slash"),
Arguments.of("spiffe://trustdomain/path/", "Path cannot have a trailing slash")
);
}
@Test
void testNewSpiffeId() {
TrustDomain trustDomain = TrustDomain.parse("test.domain");
SpiffeId spiffeId = trustDomain.newSpiffeId("path1", "host");
assertEquals(trustDomain, spiffeId.getTrustDomain());
assertEquals("/path1/host", spiffeId.getPath());
}
@Test
void testToString() {
TrustDomain trustDomain = TrustDomain.parse("test.domain");
assertEquals("test.domain", trustDomain.toString());
}
@Test
void test_toIdString() {
val trustDomain = TrustDomain.parse("domain.test");
assertEquals("spiffe://domain.test", trustDomain.toIdString());
}
}

View File

@ -0,0 +1,93 @@
package io.spiffe.svid.jwtsvid;
import io.spiffe.internal.JwtSignatureAlgorithm;
import lombok.Builder;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class JwtSignatureAlgorithmTest {
@ParameterizedTest
@MethodSource("provideTestCases")
void parse(TestCase testCase) {
JwtSignatureAlgorithm signatureAlgorithm = JwtSignatureAlgorithm.parse(testCase.name);
assertEquals(testCase.expectedAlgorithm, signatureAlgorithm);
assertEquals(testCase.name, signatureAlgorithm.getName());
}
static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(TestCase.builder().name("RS256").expectedAlgorithm(JwtSignatureAlgorithm.RS256).build()),
Arguments.of(TestCase.builder().name("RS384").expectedAlgorithm(JwtSignatureAlgorithm.RS384).build()),
Arguments.of(TestCase.builder().name("RS512").expectedAlgorithm(JwtSignatureAlgorithm.RS512).build()),
Arguments.of(TestCase.builder().name("ES256").expectedAlgorithm(JwtSignatureAlgorithm.ES256).build()),
Arguments.of(TestCase.builder().name("ES384").expectedAlgorithm(JwtSignatureAlgorithm.ES384).build()),
Arguments.of(TestCase.builder().name("ES512").expectedAlgorithm(JwtSignatureAlgorithm.ES512).build()),
Arguments.of(TestCase.builder().name("PS256").expectedAlgorithm(JwtSignatureAlgorithm.PS256).build()),
Arguments.of(TestCase.builder().name("PS384").expectedAlgorithm(JwtSignatureAlgorithm.PS384).build()),
Arguments.of(TestCase.builder().name("PS512").expectedAlgorithm(JwtSignatureAlgorithm.PS512).build())
);
}
@Value
static class TestCase {
String name;
JwtSignatureAlgorithm expectedAlgorithm;
@Builder
public TestCase(String name, JwtSignatureAlgorithm expectedAlgorithm) {
this.name = name;
this.expectedAlgorithm = expectedAlgorithm;
}
}
@Test
void testParseFamilyRSA() {
JwtSignatureAlgorithm.Family rsa = JwtSignatureAlgorithm.Family.parse("RSA");
assertEquals(JwtSignatureAlgorithm.Family.RSA, rsa);
assertTrue(rsa.contains(JwtSignatureAlgorithm.RS256));
assertTrue(rsa.contains(JwtSignatureAlgorithm.RS384));
assertTrue(rsa.contains(JwtSignatureAlgorithm.RS512));
assertTrue(rsa.contains(JwtSignatureAlgorithm.PS256));
assertTrue(rsa.contains(JwtSignatureAlgorithm.PS384));
assertTrue(rsa.contains(JwtSignatureAlgorithm.PS512));
}
@Test
void testParseFamilyEC() {
JwtSignatureAlgorithm.Family ec = JwtSignatureAlgorithm.Family.parse("EC");
assertEquals(JwtSignatureAlgorithm.Family.EC, ec);
assertTrue(ec.contains(JwtSignatureAlgorithm.ES256));
assertTrue(ec.contains(JwtSignatureAlgorithm.ES384));
assertTrue(ec.contains(JwtSignatureAlgorithm.ES512));
}
@Test
void testParseUnknownFamily() {
try {
JwtSignatureAlgorithm.Family.parse("unknown family");
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unsupported JWT family algorithm: unknown family", e.getMessage());
}
}
@Test
void testParseUnsupportedAlgorithm() {
try {
JwtSignatureAlgorithm.parse("HS256");
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unsupported JWT algorithm: HS256", e.getMessage());
}
}
}

View File

@ -0,0 +1,329 @@
package io.spiffe.svid.jwtsvid;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jwt.JWTClaimsSet;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.AuthorityNotFoundException;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.TestUtils;
import lombok.Builder;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.security.KeyPair;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static io.spiffe.svid.jwtsvid.JwtSvidParseInsecureTest.newJwtSvidInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class JwtSvidParseAndValidateTest {
private static final String HS256TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eTEifQ." +
"eyJzdWIiOiJzcGlmZmU6Ly90ZXN0LmRvbWFpbi9ob3N0IiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxMjM0MzQzNTM0NTUsImlh" +
"dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao";
@ParameterizedTest
@MethodSource("provideSuccessScenarios")
void parseAndValidateValidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid jwtSvid = JwtSvid.parseAndValidate(token, testCase.jwtBundle, testCase.audience, testCase.hint);
assertEquals(testCase.expectedJwtSvid.getSpiffeId(), jwtSvid.getSpiffeId());
assertEquals(testCase.expectedJwtSvid.getAudience(), jwtSvid.getAudience());
assertEquals(testCase.expectedJwtSvid.getHint(), jwtSvid.getHint());
assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond());
assertEquals(token, jwtSvid.getToken());
assertEquals(token, jwtSvid.marshal());
} catch (Exception e) {
fail(e);
}
}
@ParameterizedTest
@MethodSource("provideFailureScenarios")
void parseAndValidateInvalidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid.parseAndValidate(token, testCase.jwtBundle, testCase.audience);
fail("expected error: " + testCase.expectedException.getMessage());
} catch (Exception e) {
assertEquals(testCase.expectedException.getClass(), e.getClass());
assertEquals(testCase.expectedException.getMessage(), e.getMessage());
}
}
@Test
void testParseAndValidate_nullToken_throwsNullPointerException() throws JwtSvidException, AuthorityNotFoundException, BundleNotFoundException {
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
Set<String> audience = Collections.singleton("audience");
try {
JwtSvid.parseAndValidate(null, jwtBundle, audience);
} catch (NullPointerException e) {
assertEquals("token is marked non-null but is null", e.getMessage());
}
}
@Test
void testParseAndValidate_emptyToken_throwsIllegalArgumentException() throws JwtSvidException, AuthorityNotFoundException, BundleNotFoundException {
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
Set<String> audience = Collections.singleton("audience");
try {
JwtSvid.parseAndValidate("", jwtBundle, audience);
} catch (IllegalArgumentException e) {
assertEquals("Token cannot be blank", e.getMessage());
}
}
@Test
void testParseAndValidate_nullBundle_throwsNullPointerException() throws JwtSvidException, AuthorityNotFoundException, BundleNotFoundException {
Set<String> audience = Collections.singleton("audience");
try {
JwtSvid.parseAndValidate("token", null, audience);
} catch (NullPointerException e) {
assertEquals("jwtBundleSource is marked non-null but is null", e.getMessage());
}
}
@Test
void testParseAndValidate_nullAudience_throwsNullPointerException() throws JwtSvidException, AuthorityNotFoundException, BundleNotFoundException {
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
try {
JwtSvid.parseAndValidate("token", jwtBundle, null);
} catch (NullPointerException e) {
assertEquals("audience is marked non-null but is null", e.getMessage());
}
}
static Stream<Arguments> provideSuccessScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key3 = TestUtils.generateRSAKeyPair(2048);
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
jwtBundle.putJwtAuthority("authority3", key3.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date issuedAt = new Date();
Date expiration = new Date(System.currentTimeMillis() + (60 * 60 * 1000));
Set<String> audience = new HashSet<String>() {{add("audience1"); add("audience2");}};
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder()
.name("using EC signature")
.jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("audience1"))
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE))
.expectedException(null)
.hint("external")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(),
TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE),
"external"
))
.build()),
Arguments.of(TestCase.builder()
.name("using RSA signature")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority3", JwtSvid.HEADER_TYP_JWT))
.expectedException(null)
.hint("internal")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(), TestUtils.generateToken(claims, key3, "authority3", JwtSvid.HEADER_TYP_JWT),
"internal"
))
.build()),
Arguments.of(TestCase.builder()
.name("using empty typ")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority3", ""))
.expectedException(null)
.hint("")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(),
TestUtils.generateToken(claims, key3, "authority3"),
""
))
.build())
);
}
static Stream<Arguments> provideFailureScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key3 = TestUtils.generateRSAKeyPair(2048);
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
jwtBundle.putJwtAuthority("authority3", key3.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + (60 * 60 * 1000));
Set<String> audience = new HashSet<String>() {{add("audience1"); add("audience2");}};
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder()
.name("malformed")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> "invalid token")
.expectedException(new IllegalArgumentException("Unable to parse JWT token"))
.build()),
Arguments.of(TestCase.builder()
.name("unsupported algorithm")
.jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("audience"))
.generateToken(() -> HS256TOKEN)
.expectedException(new JwtSvidException("Unsupported JWT algorithm: HS256"))
.build()),
Arguments.of(TestCase.builder()
.name("missing subject")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing subject claim"))
.build()),
Arguments.of(TestCase.builder()
.name("missing expiration")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), null), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing expiration claim"))
.build()),
Arguments.of(TestCase.builder()
.name("token has expired")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), new Date()), key1, "authority1"))
.expectedException(new JwtSvidException("Token has expired"))
.build()),
Arguments.of(TestCase.builder()
.name("unexpected audience")
.jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("another"))
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new JwtSvidException("expected audience in [another] (audience=[audience2, audience1])"))
.build()),
Arguments.of(TestCase.builder()
.name("invalid subject claim")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "non-spiffe-subject", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID"))
.build()),
Arguments.of(TestCase.builder()
.name("missing key id")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, null))
.expectedException(new JwtSvidException("Token header missing key id"))
.build()),
Arguments.of(TestCase.builder()
.name("key id contains an empty value")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, " "))
.expectedException(new JwtSvidException("Token header key id contains an empty value"))
.build()),
Arguments.of(TestCase.builder()
.name("no bundle for trust domain")
.jwtBundle(new JwtBundle(TrustDomain.parse("other.domain")))
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new BundleNotFoundException("No JWT bundle found for trust domain test.domain"))
.build()),
Arguments.of(TestCase.builder()
.name("no authority found for key id")
.jwtBundle(new JwtBundle(TrustDomain.parse("test.domain")))
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new AuthorityNotFoundException("No authority found for the trust domain test.domain and key id authority1"))
.build()),
Arguments.of(TestCase.builder()
.name("signature cannot be verified with authority")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key2, "authority1"))
.expectedException(new JwtSvidException("Signature invalid: cannot be verified with the authority with keyId=authority1"))
.build()),
Arguments.of(TestCase.builder()
.name("authority algorithm mismatch")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority1"))
.expectedException(new JwtSvidException("Error verifying signature with the authority with keyId=authority1"))
.build()),
Arguments.of(TestCase.builder()
.name("not valid header 'typ'")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", "OTHER"))
.expectedException(new JwtSvidException("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: 'OTHER'."))
.build())
);
}
@Value
static class TestCase {
String name;
JwtBundle jwtBundle;
Set<String> audience;
String hint;
Supplier<String> generateToken;
Exception expectedException;
JwtSvid expectedJwtSvid;
@Builder
public TestCase(String name, JwtBundle jwtBundle, Set<String> expectedAudience, Supplier<String> generateToken,
Exception expectedException, JwtSvid expectedJwtSvid, String hint) {
this.name = name;
this.jwtBundle = jwtBundle;
this.audience = expectedAudience;
this.generateToken = generateToken;
this.expectedException = expectedException;
this.expectedJwtSvid = expectedJwtSvid;
this.hint = hint;
}
}
}

View File

@ -0,0 +1,271 @@
package io.spiffe.svid.jwtsvid;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jwt.JWTClaimsSet;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.TestUtils;
import lombok.Builder;
import lombok.Value;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.lang.reflect.InvocationTargetException;
import java.security.KeyPair;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class JwtSvidParseInsecureTest {
private static final String HS256TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eTEifQ." +
"eyJzdWIiOiJzcGlmZmU6Ly90ZXN0LmRvbWFpbi9ob3N0IiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxMjM0MzQzNTM0NTUsImlh" +
"dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao";
@ParameterizedTest
@MethodSource("provideSuccessScenarios")
void parseValidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid jwtSvid = JwtSvid.parseInsecure(token, testCase.audience, testCase.hint);
assertEquals(testCase.expectedJwtSvid.getSpiffeId(), jwtSvid.getSpiffeId());
assertEquals(testCase.expectedJwtSvid.getAudience(), jwtSvid.getAudience());
assertEquals(testCase.expectedJwtSvid.getHint(), jwtSvid.getHint());
assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond());
assertEquals(token, jwtSvid.getToken());
} catch (Exception e) {
fail(e);
}
}
@ParameterizedTest
@MethodSource("provideFailureScenarios")
void parseInvalidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid.parseInsecure(token, testCase.audience);
fail("expected error: " + testCase.expectedException.getMessage());
} catch (Exception e) {
assertEquals(testCase.expectedException.getClass(), e.getClass());
assertEquals(testCase.expectedException.getMessage(), e.getMessage());
}
}
@Test
void testParseInsecure_nullToken_throwsNullPointerException() throws JwtSvidException {
Set<String> audience = Collections.singleton("audience");
try {
JwtSvid.parseInsecure(null, audience);
} catch (NullPointerException e) {
assertEquals("token is marked non-null but is null", e.getMessage());
}
}
@Test
void testParseAndValidate_emptyToken_throwsIllegalArgumentException() throws JwtSvidException {
Set<String> audience = Collections.singleton("audience");
try {
JwtSvid.parseInsecure("", audience);
} catch (IllegalArgumentException e) {
assertEquals("Token cannot be blank", e.getMessage());
}
}
@Test
void testParseInsecure_nullAudience_throwsNullPointerException() throws JwtSvidException {
try {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
TrustDomain trustDomain = TrustDomain.parse("test.domain");
SpiffeId spiffeId = trustDomain.newSpiffeId("/host");
Set<String> audience = Collections.singleton("audience");
Date expiration = new Date(System.currentTimeMillis() + 3600000);
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
JwtSvid.parseInsecure(TestUtils.generateToken(claims, key1, "authority1"), null);
} catch (NullPointerException e) {
assertEquals("audience is marked non-null but is null", e.getMessage());
}
}
static Stream<Arguments> provideSuccessScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + 3600000);
Date issuedAt = new Date();
Set<String> audience = Collections.singleton("audience");
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder()
.name("using typ as JWT")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT))
.expectedException(null)
.hint("internal")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(),
TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT),
"internal"
))
.build()),
Arguments.of(TestCase.builder()
.name("using typ as JOSE")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE))
.expectedException(null)
.hint("external")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(),
TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT),
"external"
))
.build()),
Arguments.of(TestCase.builder()
.name("using empty typ")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", ""))
.expectedException(null)
.hint("")
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
issuedAt,
expiration,
claims.getClaims(),
TestUtils.generateToken(claims, key1, "authority1", ""),
""
))
.build()));
}
static Stream<Arguments> provideFailureScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
TrustDomain trustDomain = TrustDomain.parse("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + 3600000);
Set<String> audience = Collections.singleton("audience");
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder()
.name("malformed")
.expectedAudience(audience)
.generateToken(() -> "invalid token")
.expectedException(new IllegalArgumentException("Unable to parse JWT token"))
.build()),
Arguments.of(TestCase.builder()
.name("missing subject")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing subject claim"))
.build()),
Arguments.of(TestCase.builder()
.name("missing expiration")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), null), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing expiration claim"))
.build()),
Arguments.of(TestCase.builder()
.name("token has expired")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), new Date()), key1, "authority1"))
.expectedException(new JwtSvidException("Token has expired"))
.build()),
Arguments.of(TestCase.builder()
.name("unexpected audience")
.expectedAudience(Collections.singleton("another"))
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new JwtSvidException("expected audience in [another] (audience=[audience])"))
.build()),
Arguments.of(TestCase.builder()
.name("invalid subject claim")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "non-spiffe-subject", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID"))
.build()),
Arguments.of(TestCase.builder()
.name("not valid header 'typ'")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", "OTHER"))
.expectedException(new JwtSvidException("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: 'OTHER'."))
.build())
);
}
@Value
static class TestCase {
String name;
Set<String> audience;
String hint;
Supplier<String> generateToken;
Exception expectedException;
JwtSvid expectedJwtSvid;
@Builder
public TestCase(String name, Set<String> expectedAudience, Supplier<String> generateToken,
Exception expectedException, JwtSvid expectedJwtSvid, String hint) {
this.name = name;
this.audience = expectedAudience;
this.generateToken = generateToken;
this.expectedException = expectedException;
this.expectedJwtSvid = expectedJwtSvid;
this.hint = hint;
}
}
static JwtSvid newJwtSvidInstance(final SpiffeId spiffeId,
final Set<String> audience,
final Date issuedAt,
final Date expiry,
final Map<String, Object> claims,
final String token,
final String hint
) {
val constructor = JwtSvid.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
try {
return (JwtSvid) constructor.newInstance(spiffeId, audience, issuedAt, expiry, claims, token, hint);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,377 @@
package io.spiffe.svid.x509svid;
import io.spiffe.exception.X509SvidException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import lombok.Builder;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.util.StringUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.X509Certificate;
import java.util.stream.Stream;
import static io.spiffe.utils.TestUtils.toUri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
class X509SvidTest {
static String keyRSA = "testdata/x509svid/key-pkcs8-rsa.pem";
static String certSingle = "testdata/x509svid/good-leaf-only.pem";
static String leafNoDigitalSignature = "testdata/x509svid/wrong-leaf-no-digital-signature.pem";
static String leafCRLSign = "testdata/x509svid/wrong-leaf-crl-sign.pem";
static String leafCertSign = "testdata/x509svid/wrong-leaf-cert-sign.pem";
static String leafCAtrue = "testdata/x509svid/wrong-leaf-ca-true.pem";
static String leafEmptyID = "testdata/x509svid/wrong-leaf-empty-id.pem";
static String signNoCA = "testdata/x509svid/wrong-intermediate-no-ca.pem";
static String signNoKeyCertSign = "testdata/x509svid/wrong-intermediate-no-key-cert-sign.pem";
static String keyECDSA = "testdata/x509svid/key-pkcs8-ecdsa.pem";
static String certMultiple = "testdata/x509svid/good-leaf-and-intermediate.pem";
static String corrupted = "testdata/x509svid/corrupted";
static String keyDER = "testdata/x509svid/keyEC.der";
static String certDER = "testdata/x509svid/cert.der";
static Stream<Arguments> provideX509SvidScenarios() {
return Stream.of(
Arguments.of(TestCase
.builder()
.name("1. Single certificate and key")
.certsPath(certSingle)
.keyPath(keyRSA)
.hint("")
.expectedSpiffeId(SpiffeId.fromSegments(TrustDomain.parse("example.org"), "workload-1"))
.expectedNumberOfCerts(1)
.expectedPrivateKeyAlgorithm("RSA")
.expectedHint("")
.build()
),
Arguments.of(TestCase
.builder()
.name("2. Certificate with intermediate and key")
.certsPath(certMultiple)
.keyPath(keyECDSA)
.hint("")
.expectedSpiffeId(SpiffeId.fromSegments(TrustDomain.parse("example.org"), "workload-1"))
.expectedNumberOfCerts(2)
.expectedPrivateKeyAlgorithm("EC")
.expectedHint("")
.build()
),
Arguments.of(TestCase
.builder()
.name("3. Missing certificate")
.certsPath(keyRSA)
.keyPath(keyRSA)
.hint("")
.expectedError("Certificate could not be parsed from cert bytes")
.expectedHint("")
.build()
),
Arguments.of(TestCase
.builder()
.name("4. Missing key")
.certsPath(certSingle)
.keyPath(certSingle)
.hint("")
.expectedError("Private Key could not be parsed from key bytes")
.build()
),
Arguments.of(TestCase
.builder()
.name("5. Corrupted private key")
.certsPath(certSingle)
.keyPath(corrupted)
.hint("")
.expectedError("Private Key could not be parsed from key bytes")
.build()
),
Arguments.of(TestCase
.builder()
.name("6. Corrupted certificate")
.certsPath(corrupted)
.keyPath(keyRSA)
.hint("")
.expectedError("Certificate could not be parsed from cert bytes")
.build()
),
Arguments.of(TestCase
.builder()
.name("7. Certificate without SPIFFE ID")
.certsPath(leafEmptyID)
.keyPath(keyRSA)
.hint("")
.expectedError("Certificate does not contain SPIFFE ID in the URI SAN")
.build()
),
Arguments.of(TestCase
.builder()
.name("8. Leaf certificate with CA flag set to true")
.certsPath(leafCAtrue)
.keyPath(keyRSA)
.hint("")
.expectedError("Leaf certificate must not have CA flag set to true")
.build()
),
Arguments.of(TestCase
.builder()
.name("9. Leaf certificate without digitalSignature as key usage")
.certsPath(leafNoDigitalSignature)
.keyPath(keyRSA)
.hint("")
.expectedError("Leaf certificate must have 'digitalSignature' as key usage")
.build()
),
Arguments.of(TestCase
.builder()
.name("10. Leaf certificate with certSign as key usage")
.certsPath(leafCertSign)
.keyPath(keyRSA)
.hint("")
.expectedError("Leaf certificate must not have 'keyCertSign' as key usage")
.build()
),
Arguments.of(TestCase
.builder()
.name("11. Leaf certificate with cRLSign as key usage")
.certsPath(leafCRLSign)
.keyPath(keyRSA)
.hint("")
.expectedError("Leaf certificate must not have 'cRLSign' as key usage")
.build()
),
Arguments.of(TestCase
.builder()
.name("12. Signing certificate without CA flag")
.certsPath(signNoCA)
.keyPath(keyRSA)
.hint("")
.expectedError("Signing certificate must have CA flag set to true")
.build()
),
Arguments.of(TestCase
.builder()
.name("13. Signing certificate without CA flag")
.certsPath(signNoKeyCertSign)
.keyPath(keyRSA)
.hint("")
.expectedError("Signing certificate must have 'keyCertSign' as key usage")
.build()
),
Arguments.of(TestCase
.builder()
.name("14. SVID with non-empty hint")
.certsPath(certSingle)
.keyPath(keyRSA)
.hint("internal")
.expectedSpiffeId(SpiffeId.fromSegments(TrustDomain.parse("example.org"), "workload-1"))
.expectedNumberOfCerts(1)
.expectedPrivateKeyAlgorithm("RSA")
.expectedHint("internal")
.build()
)
);
}
@Test
void testLoad_Success() throws URISyntaxException {
Path certPath = Paths.get(toUri(certSingle));
Path keyPath = Paths.get(toUri(keyRSA));
try {
X509Svid x509Svid = X509Svid.load(certPath, keyPath);
assertEquals("spiffe://example.org/workload-1", x509Svid.getSpiffeId().toString());
} catch (X509SvidException e) {
fail(e);
}
}
@Test
void testParseRaw() throws URISyntaxException, IOException {
Path certPath = Paths.get(toUri(certDER));
Path keyPath = Paths.get(toUri(keyDER));
byte[] certBytes = Files.readAllBytes(certPath);
byte[] keyBytes = Files.readAllBytes(keyPath);
try {
X509Svid x509Svid = X509Svid.parseRaw(certBytes, keyBytes, "external");
assertEquals("spiffe://example.org/workload-server", x509Svid.getSpiffeId().toString());
assertEquals("external", x509Svid.getHint());
} catch (X509SvidException e) {
fail(e);
}
}
@Test
void testParseRawNullCertsBytesArgument() {
try {
X509Svid.parseRaw(null, new byte[]{});
fail();
} catch (NullPointerException e) {
assertEquals("certsBytes is marked non-null but is null", e.getMessage());
} catch (X509SvidException e) {
fail();
}
}
@Test
void testParseRawNullPrivateKeyBytesArgument() {
try {
X509Svid.parseRaw(new byte[]{}, null);
fail();
} catch (NullPointerException e) {
assertEquals("privateKeyBytes is marked non-null but is null", e.getMessage());
} catch (X509SvidException e) {
fail();
}
}
@Test
void testLoad_FailsCannotReadCertFile() throws URISyntaxException {
Path keyPath = Paths.get(toUri(keyRSA));
try {
X509Svid.load(Paths.get("not-existent-cert"), keyPath);
fail("should have thrown IOException");
} catch (X509SvidException e) {
assertEquals("Cannot read certificate file", e.getMessage());
}
}
@Test
void testLoad_FailsCannotReadKeyFile() throws URISyntaxException {
Path certPath = Paths.get(toUri(certSingle));
try {
X509Svid.load(certPath, Paths.get("not-existent-key"));
fail("should have thrown IOException");
} catch (X509SvidException e) {
assertEquals("Cannot read private key file", e.getMessage());
}
}
@Test
void testLoad_nullCertFilePath_throwsNullPointerException() throws URISyntaxException {
try {
X509Svid.load(null, Paths.get(toUri(keyRSA)));
fail("should have thrown exception");
} catch (NullPointerException | X509SvidException e) {
assertEquals("certsFilePath is marked non-null but is null", e.getMessage());
}
}
@Test
void testLoad_nullKeyFilePath_throwsNullPointerException() throws URISyntaxException, X509SvidException {
try {
X509Svid.load(Paths.get(toUri(certSingle)), null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("privateKeyFilePath is marked non-null but is null", e.getMessage());
}
}
@Test
void testParse_nullByteArray_throwsNullPointerException() throws X509SvidException {
try {
X509Svid.parse(null, "key".getBytes());
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("certsBytes is marked non-null but is null", e.getMessage());
}
}
@Test
void testParse_nullKeyByteArray_throwsNullPointerException() throws X509SvidException {
try {
X509Svid.parse("cert".getBytes(), null);
fail("should have thrown exception");
} catch (NullPointerException e) {
assertEquals("privateKeyBytes is marked non-null but is null", e.getMessage());
}
}
@Test
void testGetLeaf() throws URISyntaxException, X509SvidException {
Path certPath = Paths.get(toUri(certSingle));
Path keyPath = Paths.get(toUri(keyRSA));
X509Svid x509Svid = X509Svid.load(certPath, keyPath);
assertEquals(x509Svid.getChain().get(0), x509Svid.getLeaf());
}
@Test
void testGetChainArray() throws URISyntaxException, X509SvidException {
Path certPath = Paths.get(toUri(certMultiple));
Path keyPath = Paths.get(toUri(keyECDSA));
X509Svid x509Svid = X509Svid.load(certPath, keyPath);
X509Certificate[] x509CertificatesArray = x509Svid.getChainArray();
assertEquals(x509Svid.getChain().get(0), x509CertificatesArray[0]);
assertEquals(x509Svid.getChain().get(1), x509CertificatesArray[1]);
}
@ParameterizedTest
@MethodSource("provideX509SvidScenarios")
void parseX509Svid(TestCase testCase) {
try {
Path certPath = Paths.get(toUri(testCase.certsPath));
Path keyPath = Paths.get(toUri(testCase.keyPath));
byte[] certBytes = Files.readAllBytes(certPath);
byte[] keyBytes = Files.readAllBytes(keyPath);
X509Svid x509Svid = X509Svid.parse(certBytes, keyBytes, testCase.getHint());
if (StringUtils.isNotBlank(testCase.expectedError)) {
fail(String.format("Error was expected: %s", testCase.expectedError));
}
assertNotNull(x509Svid);
assertNotNull(x509Svid.getSpiffeId());
assertNotNull(x509Svid.getChain());
assertNotNull(x509Svid.getPrivateKey());
assertEquals(testCase.expectedNumberOfCerts, x509Svid.getChain().size());
assertEquals(testCase.expectedSpiffeId, x509Svid.getSpiffeId());
assertEquals(testCase.expectedPrivateKeyAlgorithm, x509Svid.getPrivateKey().getAlgorithm());
assertEquals(testCase.expectedHint, x509Svid.getHint());
} catch (Exception e) {
if (StringUtils.isBlank(testCase.expectedError)) {
fail(e);
}
assertEquals(testCase.expectedError, e.getMessage());
}
}
@Value
static class TestCase {
String name;
String certsPath;
String hint;
String keyPath;
SpiffeId expectedSpiffeId;
int expectedNumberOfCerts;
String expectedPrivateKeyAlgorithm;
String expectedHint;
String expectedError;
@Builder
public TestCase(String name, String certsPath, String keyPath, String hint, SpiffeId expectedSpiffeId, int expectedNumberOfCerts, String expectedPrivateKeyAlgorithm, String expectedHint, String expectedError) {
this.name = name;
this.certsPath = certsPath;
this.keyPath = keyPath;
this.hint = hint;
this.expectedSpiffeId = expectedSpiffeId;
this.expectedNumberOfCerts = expectedNumberOfCerts;
this.expectedPrivateKeyAlgorithm = expectedPrivateKeyAlgorithm;
this.expectedError = expectedError;
this.expectedHint = expectedHint;
}
}
}

View File

@ -0,0 +1,157 @@
package io.spiffe.svid.x509svid;
import com.google.common.collect.Sets;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.CertAndKeyPair;
import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificate;
import static io.spiffe.utils.X509CertificateTestUtils.createRootCA;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class X509SvidValidatorTest {
List<X509Certificate> chain;
CertAndKeyPair rootCa;
CertAndKeyPair otherRootCa;
CertAndKeyPair leaf;
@BeforeEach
void setUp() throws Exception {
rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://example.org" );
val intermediate1 = createCertificate("C = US, O = SPIRE", "C = US, O = SPIFFE", "spiffe://example.org/host", rootCa, true);
val intermediate2 = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://example.org/host2", intermediate1, true);
leaf = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://example.org/test", intermediate2, false);
chain = Arrays.asList(leaf.getCertificate(), intermediate2.getCertificate(), intermediate1.getCertificate());
otherRootCa = createRootCA("C = US, O = SPIFFE", "spiffe://example.org" );
}
@Test
void testVerifyChain_chainCanBeVerifiedWithAuthorityInBundle() throws Exception {
HashSet<X509Certificate> x509Authorities = new HashSet<>();
x509Authorities.add(rootCa.getCertificate());
x509Authorities.add(otherRootCa.getCertificate());
val x509Bundle = new X509Bundle(TrustDomain.parse("example.org"), x509Authorities);
X509SvidValidator.verifyChain(chain, x509Bundle);
}
@Test
void testVerifyChain_chainCannotBeVerifiedWithAuthorityInBundle_throwsCertificateException() throws Exception {
HashSet<X509Certificate> x509Authorities = new HashSet<>();
x509Authorities.add(otherRootCa.getCertificate());
val x509Bundle = new X509Bundle(TrustDomain.parse("example.org"), x509Authorities);
try {
X509SvidValidator.verifyChain(chain, x509Bundle);
fail("exception is expected");
} catch (CertificateException e) {
assertEquals("Cert chain cannot be verified", e.getMessage());
}
}
@Test
void verifyChain_noBundleForTrustDomain_throwsBundleNotFoundException() throws Exception {
HashSet<X509Certificate> x509Authorities = new HashSet<>();
x509Authorities.add(otherRootCa.getCertificate());
val x509Bundle = new X509Bundle(TrustDomain.parse("other.org"), x509Authorities);
try {
X509SvidValidator.verifyChain(chain, x509Bundle);
fail("Verify chain should have thrown validation exception");
} catch (BundleNotFoundException e) {
assertEquals("No X.509 bundle found for trust domain example.org", e.getMessage());
}
}
@Test
void verifySpiffeId_givenASpiffeIdInTheListOfAcceptedIds_doesntThrowException() throws IOException, CertificateException, URISyntaxException {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/test");
val spiffeId2 = SpiffeId.parse("spiffe://example.org/test2");
val spiffeIdSet = Sets.newHashSet(spiffeId1, spiffeId2);
X509SvidValidator.verifySpiffeId(leaf.getCertificate(), () -> spiffeIdSet);
}
@Test
void verifySpiffeId_givenASpiffeIdNotInTheListOfAcceptedIds_throwsCertificateException() throws IOException, CertificateException, URISyntaxException {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/other1");
val spiffeId2 = SpiffeId.parse("spiffe://example.org/other2");
val spiffeIdSet = Sets.newHashSet(spiffeId1, spiffeId2);
try {
X509SvidValidator.verifySpiffeId(leaf.getCertificate(), () -> spiffeIdSet);
fail("Should have thrown CertificateException");
} catch (CertificateException e) {
assertEquals("SPIFFE ID spiffe://example.org/test in X.509 certificate is not accepted", e.getMessage());
}
}
@Test
void verifySpiffeId_givenAnEmptySupplier_throwsCertificateException() {
try {
X509SvidValidator.verifySpiffeId(leaf.getCertificate(), Collections::emptySet);
fail("Should have thrown CertificateException");
} catch (CertificateException e) {
assertEquals("The supplier of accepted SPIFFE IDs supplied an empty set", e.getMessage());
}
}
@Test
void checkSpiffeId_nullX509Certificate_throwsNullPointerException() throws CertificateException {
try {
X509SvidValidator.verifySpiffeId(null, Collections::emptySet);
fail("should have thrown an exception");
} catch (NullPointerException e) {
assertEquals("x509Certificate is marked non-null but is null", e.getMessage());
}
}
@Test
void checkSpiffeId_nullAcceptedSpiffeIdsSuppplier_throwsNullPointerException() throws CertificateException, URISyntaxException, IOException {
try {
X509SvidValidator.verifySpiffeId(leaf.getCertificate(), null);
fail("should have thrown an exception");
} catch (NullPointerException e) {
assertEquals("acceptedSpiffeIdsSupplier is marked non-null but is null", e.getMessage());
}
}
@Test
void verifyChain_nullChain_throwsNullPointerException() throws CertificateException, BundleNotFoundException {
try {
X509SvidValidator.verifyChain(null, new X509Bundle(TrustDomain.parse("example.org")));
fail("should have thrown an exception");
} catch (NullPointerException e) {
assertEquals("chain is marked non-null but is null", e.getMessage());
}
}
@Test
void verifyChain_nullBundleSource_throwsNullPointerException() throws CertificateException, BundleNotFoundException, URISyntaxException, IOException {
try {
X509SvidValidator.verifyChain(chain, null);
fail("should have thrown an exception");
} catch (NullPointerException e) {
assertEquals("x509BundleSource is marked non-null but is null", e.getMessage());
}
}
}

View File

@ -0,0 +1,98 @@
package io.spiffe.workloadapi;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.utils.TestUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import java.net.URI;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class AddressTest {
@ParameterizedTest
@MethodSource("provideTestAddress")
void parseAddress(String input, Object expected) {
URI result = null;
try {
result = Address.parseAddress(input);
assertEquals(expected, result);
} catch (SocketEndpointAddressException e) {
assertEquals(expected, e.getMessage());
}
}
@Test
void parseAddress_nullArgument() {
try {
Address.parseAddress(null);
} catch (NullPointerException e) {
assertEquals("address is marked non-null but is null", e.getMessage());
} catch (SocketEndpointAddressException e) {
fail();
}
}
static Stream<Arguments> provideTestAddress() {
return Stream.of(
Arguments.of("", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: "),
Arguments.of("unix:///foo", URI.create("unix:///foo")),
Arguments.of("unix:/path/to/endpoint.sock", URI.create("unix:/path/to/endpoint.sock")),
Arguments.of("unix:///path/to/endpoint.sock", URI.create("unix:///path/to/endpoint.sock")),
Arguments.of("tcp://127.0.0.1:8000", URI.create("tcp://127.0.0.1:8000")),
Arguments.of("", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: "),
Arguments.of("\\t", "Workload endpoint socket is not a valid URI: \\t"),
Arguments.of("///foo", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: ///foo"),
Arguments.of("blah", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: blah"),
Arguments.of("blah:///foo", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: blah:///foo"),
Arguments.of("unix:opaque", "Workload endpoint unix socket URI must not be opaque: unix:opaque"),
Arguments.of("unix:/", "Workload endpoint unix socket path cannot be blank: unix:/"),
Arguments.of("unix://", "Workload endpoint socket is not a valid URI: unix://"),
Arguments.of("unix:///", "Workload endpoint unix socket path cannot be blank: unix:///"),
Arguments.of("unix://foo", "Workload endpoint unix socket URI must not include authority component: unix://foo"),
Arguments.of("unix:///foo?whatever", "Workload endpoint unix socket URI must not include query values: unix:///foo?whatever"),
Arguments.of("unix:///foo#whatever", "Workload endpoint unix socket URI must not include a fragment: unix:///foo#whatever"),
Arguments.of("tcp://127.0.0.1:8000/foo", "Workload endpoint tcp socket URI must not include a path: tcp://127.0.0.1:8000/foo"),
Arguments.of("tcp:opaque", "Workload endpoint tcp socket URI must not be opaque: tcp:opaque"),
Arguments.of("tcp://", "Workload endpoint socket is not a valid URI: tcp://"),
Arguments.of("tcp:///test", "Workload endpoint tcp socket URI must include a host: tcp:///test"),
Arguments.of("tcp://1.2.3.4:5?whatever", "Workload endpoint tcp socket URI must not include query values: tcp://1.2.3.4:5?whatever"),
Arguments.of("tcp://1.2.3.4:5#whatever", "Workload endpoint tcp socket URI must not include a fragment: tcp://1.2.3.4:5#whatever"),
Arguments.of("tcp://john:doe@1.2.3.4:5/path", "Workload endpoint tcp socket URI must not include user info: tcp://john:doe@1.2.3.4:5/path"),
Arguments.of("tcp://foo:9000", "Workload endpoint tcp socket URI host component must be an IP:port: tcp://foo:9000"),
Arguments.of("tcp://1.2.3.4", "Workload endpoint tcp socket URI host component must include a port: tcp://1.2.3.4")
);
}
@Test
void getDefaultAddress() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/test").execute(() -> {
String defaultAddress = Address.getDefaultAddress();
assertEquals("unix:/tmp/test", defaultAddress);
});
}
@Test
void getDefaultAddress_isBlankThrowsException() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "").execute(() -> {
try {
Address.getDefaultAddress();
fail();
} catch (Exception e) {
assertEquals("Endpoint Socket Address Environment Variable is not set: SPIFFE_ENDPOINT_SOCKET", e.getMessage());
}
});
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "").execute(() -> {
});
}
}

View File

@ -0,0 +1,523 @@
package io.spiffe.workloadapi;
import com.google.common.collect.Sets;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtSourceException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.jwtsvid.JwtSvid;
import lombok.val;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static io.spiffe.workloadapi.WorkloadApiClientStub.JWT_TTL;
import static org.junit.jupiter.api.Assertions.*;
class CachedJwtSourceTest {
private CachedJwtSource jwtSource;
private WorkloadApiClientStub workloadApiClient;
private WorkloadApiClientErrorStub workloadApiClientErrorStub;
private Clock clock;
@BeforeEach
void setUp() throws JwtSourceException, SocketEndpointAddressException {
workloadApiClient = new WorkloadApiClientStub();
JwtSourceOptions options = JwtSourceOptions.builder().workloadApiClient(workloadApiClient).build();
System.setProperty(CachedJwtSource.TIMEOUT_SYSTEM_PROPERTY, "PT1S");
jwtSource = (CachedJwtSource) CachedJwtSource.newSource(options);
workloadApiClientErrorStub = new WorkloadApiClientErrorStub();
clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
workloadApiClient.setClock(clock);
jwtSource.setClock(clock);
}
@AfterEach
void tearDown() throws IOException {
jwtSource.close();
}
@Test
void testGetBundleForTrustDomain() {
try {
JwtBundle bundle = jwtSource.getBundleForTrustDomain(TrustDomain.parse("example.org"));
assertNotNull(bundle);
assertEquals(TrustDomain.parse("example.org"), bundle.getTrustDomain());
} catch (BundleNotFoundException e) {
fail(e);
}
}
@Test
void testGetBundleForTrustDomain_nullParam() {
try {
jwtSource.getBundleForTrustDomain(null);
fail();
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
} catch (BundleNotFoundException e) {
fail();
}
}
@Test
void testGetBundleForTrustDomain_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.getBundleForTrustDomain(TrustDomain.parse("example.org"));
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT bundle source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (BundleNotFoundException e) {
fail("not expected exception", e);
}
}
@Test
void testFetchJwtSvidWithSubject() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithSubject_ReturnFromCache() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud3", "aud2", "aud1");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again to get from cache changing the order of the audiences
svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again using different subject
svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/extra-workload-server"), "aud2", "aud3", "aud1");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
// call again using the same audiences
svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/extra-workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithSubject_JwtSvidExpiredInCache() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// set clock forwards but not enough to expire the JWT SVID in the cache
jwtSource.setClock(clock.offset(clock, JWT_TTL.dividedBy(2).minus(Duration.ofSeconds(1))));
// call again to get from cache, fetchJwtSvid call count should not change
svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// set clock to expire the JWT SVID in the cache
jwtSource.setClock(clock.offset(clock, JWT_TTL.dividedBy(2).plus(Duration.ofSeconds(1))));
// call again, fetchJwtSvid call count should increase
svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithSubject_JwtSvidExpiredInCache_MultipleThreads() {
// test fetchJwtSvid with several threads trying to read and write the cache
// at the same time, the cache should be updated only once
try {
jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// set clock to expire the JWT SVID in the cache
Clock offset = Clock.offset(clock, JWT_TTL.dividedBy(2).plus(Duration.ofSeconds(1)));
jwtSource.setClock(offset);
workloadApiClient.setClock(offset);
// create a thread pool with 10 threads
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Future<JwtSvid>> futures = new ArrayList<>();
// create 10 tasks to fetch a JWT SVID
for (int i = 0; i < 10; i++) {
futures.add(executorService.submit(() -> jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3")));
}
// wait for all tasks to finish
for (Future<JwtSvid> future : futures) {
future.get();
}
// verify that the cache was updated only once after the JWT SVID expired
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (InterruptedException | ExecutionException | JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithoutSubject() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithoutSubject_ReturnFromCache() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again to get from cache changing the order of the audiences, the call count should not change
svid = jwtSource.fetchJwtSvid("aud3", "aud2", "aud1");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again using different audience, the call count should increase
svid = jwtSource.fetchJwtSvid("other-audience");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("other-audience"), svid.getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithoutSubject_JwtSvidExpiredInCache() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// set clock forwards but not enough to expire the JWT SVID in the cache
jwtSource.setClock(clock.offset(clock, JWT_TTL.dividedBy(2).minus(Duration.ofSeconds(1))));
// call again to get from cache, fetchJwtSvid call count should not change
svid = jwtSource.fetchJwtSvid("aud3", "aud2", "aud1");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// set clock forwards to expire the JWT SVID in the cache
jwtSource.setClock(clock.offset(clock, JWT_TTL.dividedBy(2).plus(Duration.ofSeconds(1))));
// call again, fetchJwtSvid call count should increase
svid = jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvid_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithSubject_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithSubject() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(1, svids.size());
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithSubject_ReturnFromCache() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(1, svids.size());
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again to get from cache changing the order of the audiences
svids = jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(1, svids.size());
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud3", "aud2", "aud1"), svids.get(0).getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again using different audience
svids = jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "other-audience");
assertNotNull(svids);
assertEquals(1, svids.size());
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("other-audience"), svids.get(0).getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithoutSubject() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids("aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(svids.size(), 2);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svids.get(1).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(1).getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithoutSubject_ReturnFromCache() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids("aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(svids.size(), 2);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svids.get(1).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(1).getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again to get from cache changing the order of the audiences
svids = jwtSource.fetchJwtSvids("aud2", "aud3", "aud1");
assertNotNull(svids);
assertEquals(svids.size(), 2);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svids.get(1).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(1).getAudience());
assertEquals(1, workloadApiClient.getFetchJwtSvidCallCount());
// call again using different audience
svids = jwtSource.fetchJwtSvids("other-audience");
assertNotNull(svids);
assertEquals(svids.size(), 2);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("other-audience"), svids.get(0).getAudience());
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svids.get(1).getSpiffeId());
assertEquals(Sets.newHashSet("other-audience"), svids.get(1).getAudience());
assertEquals(2, workloadApiClient.getFetchJwtSvidCallCount());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvids_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvids("aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithSubject_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void newSource_success() {
val options = JwtSourceOptions
.builder()
.workloadApiClient(workloadApiClient)
.initTimeout(Duration.ofSeconds(0))
.build();
try {
JwtSource jwtSource = CachedJwtSource.newSource(options);
assertNotNull(jwtSource);
} catch (SocketEndpointAddressException | JwtSourceException e) {
fail(e);
}
}
@Test
void newSource_nullParam() {
try {
CachedJwtSource.newSource(null);
fail();
} catch (NullPointerException e) {
assertEquals("options is marked non-null but is null", e.getMessage());
} catch (SocketEndpointAddressException | JwtSourceException e) {
fail();
}
}
@Test
void newSource_errorFetchingJwtBundles() {
val options = JwtSourceOptions
.builder()
.workloadApiClient(workloadApiClientErrorStub)
.spiffeSocketPath("unix:/tmp/test")
.build();
try {
CachedJwtSource.newSource(options);
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
assertEquals("Error fetching JwtBundleSet", e.getCause().getMessage());
} catch (Exception e) {
fail();
}
}
@Test
void newSource_FailsBecauseOfTimeOut() throws Exception {
try {
val options = JwtSourceOptions
.builder()
.spiffeSocketPath("unix:/tmp/test")
.build();
CachedJwtSource.newSource(options);
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
assertEquals("Timeout waiting for JWT bundles update", e.getCause().getMessage());
} catch (SocketEndpointAddressException e) {
fail();
}
}
@Test
void newSource_DefaultSocketAddress() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/test").execute(() -> {
try {
CachedJwtSource.newSource();
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
} catch (SocketEndpointAddressException e) {
fail();
}
});
}
@Test
void newSource_noSocketAddress() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "").execute(() -> {
try {
CachedJwtSource.newSource();
fail();
} catch (SocketEndpointAddressException e) {
fail();
} catch (IllegalStateException e) {
assertEquals("Endpoint Socket Address Environment Variable is not set: SPIFFE_ENDPOINT_SOCKET", e.getMessage());
}
});
}
}

View File

@ -0,0 +1,286 @@
package io.spiffe.workloadapi;
import com.google.common.collect.Sets;
import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtSourceException;
import io.spiffe.exception.JwtSvidException;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.jwtsvid.JwtSvid;
import io.spiffe.utils.TestUtils;
import lombok.val;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class DefaultJwtSourceTest {
private JwtSource jwtSource;
private WorkloadApiClientStub workloadApiClient;
private WorkloadApiClientErrorStub workloadApiClientErrorStub;
@BeforeEach
void setUp() throws JwtSourceException, SocketEndpointAddressException {
workloadApiClient = new WorkloadApiClientStub();
JwtSourceOptions options = JwtSourceOptions.builder().workloadApiClient(workloadApiClient).build();
System.setProperty(DefaultJwtSource.TIMEOUT_SYSTEM_PROPERTY, "PT1S");
jwtSource = DefaultJwtSource.newSource(options);
workloadApiClientErrorStub = new WorkloadApiClientErrorStub();
}
@AfterEach
void tearDown() throws IOException {
jwtSource.close();
}
@Test
void testGetBundleForTrustDomain() {
try {
JwtBundle bundle = jwtSource.getBundleForTrustDomain(TrustDomain.parse("example.org"));
assertNotNull(bundle);
assertEquals(TrustDomain.parse("example.org"), bundle.getTrustDomain());
} catch (BundleNotFoundException e) {
fail(e);
}
}
@Test
void testGetBundleForTrustDomain_nullParam() {
try {
jwtSource.getBundleForTrustDomain(null);
fail();
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
} catch (BundleNotFoundException e) {
fail();
}
}
@Test
void testGetBundleForTrustDomain_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.getBundleForTrustDomain(TrustDomain.parse("example.org"));
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT bundle source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (BundleNotFoundException e) {
fail("not expected exception", e);
}
}
@Test
void testFetchJwtSvidWithSubject() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals("external", svid.getHint());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithoutSubject() {
try {
JwtSvid svid = jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
assertNotNull(svid);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svid.getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svid.getAudience());
assertEquals("external", svid.getHint());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvid_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvid("aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidWithSubject_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithSubject() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(svids.size(), 1);
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithoutSubject() {
try {
List<JwtSvid> svids = jwtSource.fetchJwtSvids("aud1", "aud2", "aud3");
assertNotNull(svids);
assertEquals(2, svids.size());
assertEquals(SpiffeId.parse("spiffe://example.org/workload-server"), svids.get(0).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(0).getAudience());
assertEquals(SpiffeId.parse("spiffe://example.org/extra-workload-server"), svids.get(1).getSpiffeId());
assertEquals(Sets.newHashSet("aud1", "aud2", "aud3"), svids.get(1).getAudience());
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvids_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvids("aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void testFetchJwtSvidsWithSubject_SourceIsClosed_ThrowsIllegalStateException() throws IOException {
jwtSource.close();
try {
jwtSource.fetchJwtSvids(SpiffeId.parse("spiffe://example.org/workload-server"), "aud1", "aud2", "aud3");
fail("expected exception");
} catch (IllegalStateException e) {
assertEquals("JWT SVID source is closed", e.getMessage());
assertTrue(workloadApiClient.closed);
} catch (JwtSvidException e) {
fail(e);
}
}
@Test
void newSource_success() {
val options = JwtSourceOptions
.builder()
.workloadApiClient(workloadApiClient)
.initTimeout(Duration.ofSeconds(0))
.build();
try {
JwtSource jwtSource = DefaultJwtSource.newSource(options);
assertNotNull(jwtSource);
} catch (SocketEndpointAddressException | JwtSourceException e) {
fail(e);
}
}
@Test
void newSource_nullParam() {
try {
DefaultJwtSource.newSource(null);
fail();
} catch (NullPointerException e) {
assertEquals("options is marked non-null but is null", e.getMessage());
} catch (SocketEndpointAddressException | JwtSourceException e) {
fail();
}
}
@Test
void newSource_errorFetchingJwtBundles() {
val options = JwtSourceOptions
.builder()
.workloadApiClient(workloadApiClientErrorStub)
.spiffeSocketPath("unix:/tmp/test")
.build();
try {
DefaultJwtSource.newSource(options);
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
assertEquals("Error fetching JwtBundleSet", e.getCause().getMessage());
} catch (Exception e) {
fail();
}
}
@Test
void newSource_FailsBecauseOfTimeOut() throws Exception {
try {
val options = JwtSourceOptions
.builder()
.spiffeSocketPath("unix:/tmp/test")
.build();
DefaultJwtSource.newSource(options);
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
assertEquals("Timeout waiting for JWT bundles update", e.getCause().getMessage());
} catch (SocketEndpointAddressException e) {
fail();
}
}
@Test
void newSource_DefaultSocketAddress() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/test").execute(() -> {
try {
DefaultJwtSource.newSource();
fail();
} catch (JwtSourceException e) {
assertEquals("Error creating JWT source", e.getMessage());
} catch (SocketEndpointAddressException e) {
fail();
}
});
}
@Test
void newSource_noSocketAddress() throws Exception {
new EnvironmentVariables(Address.SOCKET_ENV_VARIABLE, "").execute(() -> {
try {
DefaultJwtSource.newSource();
fail();
} catch (SocketEndpointAddressException e) {
fail();
} catch (IllegalStateException e) {
assertEquals("Endpoint Socket Address Environment Variable is not set: SPIFFE_ENDPOINT_SOCKET", e.getMessage());
}
});
}
}

Some files were not shown because too many files have changed in this diff Show More