-
Notifications
You must be signed in to change notification settings - Fork 5
add support types for license verification #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
110 changes: 110 additions & 0 deletions
110
kafka-client/src/main/java/dev/responsive/kafka/internal/license/LicenseAuthenticator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import dev.responsive.kafka.internal.license.exception.LicenseAuthenticationException; | ||
| import dev.responsive.kafka.internal.license.model.LicenseDocument; | ||
| import dev.responsive.kafka.internal.license.model.LicenseDocumentV1; | ||
| import dev.responsive.kafka.internal.license.model.LicenseInfo; | ||
| import dev.responsive.kafka.internal.license.model.SigningKeys; | ||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.net.URISyntaxException; | ||
| import java.security.GeneralSecurityException; | ||
| import java.security.KeyFactory; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.security.PublicKey; | ||
| import java.security.Signature; | ||
| import java.security.spec.InvalidKeySpecException; | ||
| import java.security.spec.MGF1ParameterSpec; | ||
| import java.security.spec.PSSParameterSpec; | ||
| import java.security.spec.X509EncodedKeySpec; | ||
| import java.util.Objects; | ||
|
|
||
| public class LicenseAuthenticator { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
|
||
| private final SigningKeys signingKeys; | ||
|
|
||
| public LicenseAuthenticator(final SigningKeys signingKeys) { | ||
| this.signingKeys = Objects.requireNonNull(signingKeys); | ||
| } | ||
|
|
||
| public LicenseInfo authenticate(final LicenseDocument license) { | ||
| if (license instanceof LicenseDocumentV1) { | ||
| return authenticateLicenseV1((LicenseDocumentV1) license); | ||
| } else { | ||
| throw new IllegalArgumentException( | ||
| "unrecognized license doc type: " + license.getClass().getName()); | ||
| } | ||
| } | ||
|
|
||
| private LicenseInfo authenticateLicenseV1(final LicenseDocumentV1 license) { | ||
| final byte[] infoBytes = verifyLicenseV1Signature(license); | ||
| try { | ||
| return MAPPER.readValue(infoBytes, LicenseInfo.class); | ||
| } catch (final IOException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| private byte[] verifyLicenseV1Signature(final LicenseDocumentV1 license) { | ||
| if (!license.algo().equals("RSASSA_PSS_SHA_256")) { | ||
| throw new IllegalArgumentException("unrecognized license algo: " + license.algo()); | ||
| } | ||
| final PublicKey publicKey = loadPublicKey(signingKeys.lookupKey(license.key())); | ||
| final Signature signature; | ||
| try { | ||
| signature = Signature.getInstance("RSASSA-PSS"); | ||
| signature.setParameter( | ||
| new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1) | ||
| ); | ||
| signature.initVerify(publicKey); | ||
| final byte[] info = license.decodeInfo(); | ||
| signature.update(info); | ||
| if (!signature.verify(license.decodeSignature())) { | ||
| throw new LicenseAuthenticationException("license info did not match signature"); | ||
| } | ||
| return info; | ||
| } catch (GeneralSecurityException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| private PublicKey loadPublicKey(final SigningKeys.SigningKey signingKey) { | ||
| final File file; | ||
| try { | ||
| file = new File(this.getClass().getClassLoader().getResource(signingKey.path()).toURI()); | ||
| } catch (final URISyntaxException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| final byte[] publicKeyBytes = PublicKeyPemFileParser.parsePemFile(file); | ||
| final KeyFactory keyFactory; | ||
| try { | ||
| keyFactory = KeyFactory.getInstance("RSA"); | ||
| } catch (final NoSuchAlgorithmException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes); | ||
| try { | ||
| return keyFactory.generatePublic(keySpec); | ||
| } catch (final InvalidKeySpecException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
kafka-client/src/main/java/dev/responsive/kafka/internal/license/LicenseChecker.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license; | ||
|
|
||
| import dev.responsive.kafka.internal.license.exception.LicenseUseViolationException; | ||
| import dev.responsive.kafka.internal.license.model.LicenseInfo; | ||
| import dev.responsive.kafka.internal.license.model.TimedTrialV1; | ||
| import java.time.Instant; | ||
|
|
||
| public class LicenseChecker { | ||
| public void checkLicense(final LicenseInfo licenseInfo) { | ||
| if (licenseInfo instanceof TimedTrialV1) { | ||
| verifyTimedTrialV1((TimedTrialV1) licenseInfo); | ||
| } else { | ||
| throw new IllegalArgumentException( | ||
| "unsupported license type: " + licenseInfo.getClass().getName()); | ||
| } | ||
| } | ||
|
|
||
| private void verifyTimedTrialV1(final TimedTrialV1 timedTrial) { | ||
| final Instant expiresAt = Instant.ofEpochSecond(timedTrial.expiresAt()); | ||
| if (Instant.now().isAfter(expiresAt)) { | ||
| throw new LicenseUseViolationException("license expired at: " + expiresAt); | ||
| } | ||
| } | ||
| } | ||
56 changes: 56 additions & 0 deletions
56
kafka-client/src/main/java/dev/responsive/kafka/internal/license/PublicKeyPemFileParser.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.nio.file.Files; | ||
| import java.util.Base64; | ||
| import java.util.List; | ||
|
|
||
| public class PublicKeyPemFileParser { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could have used bouncycastle to parse these, but I didn't want to bring it in as a dependency just to do that because it's likely the user will also depend on bouncycastle (possibly at a conflicting version). |
||
| private static final String HEADER_PREFIX = "-----"; | ||
| private static final String BEGIN_PUBLIC_KEY = "BEGIN PUBLIC KEY"; | ||
| private static final String END_PUBLIC_KEY = "END PUBLIC KEY"; | ||
| private static final String BEGIN_PUBLIC_KEY_HEADER | ||
| = HEADER_PREFIX + BEGIN_PUBLIC_KEY + HEADER_PREFIX; | ||
| private static final String END_PUBLIC_KEY_HEADER | ||
| = HEADER_PREFIX + END_PUBLIC_KEY + HEADER_PREFIX; | ||
|
|
||
| public static byte[] parsePemFile(final File file) { | ||
| final List<String> lines; | ||
| try { | ||
| lines = Files.readAllLines(file.toPath()); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| final StringBuilder keyB64Builder = new StringBuilder(); | ||
| boolean foundBegin = false; | ||
| for (final String l : lines) { | ||
| if (l.equals(BEGIN_PUBLIC_KEY_HEADER)) { | ||
| foundBegin = true; | ||
| } else if (foundBegin) { | ||
| if (l.equals(END_PUBLIC_KEY_HEADER)) { | ||
| final String keyB64 = keyB64Builder.toString(); | ||
| return Base64.getDecoder().decode(keyB64); | ||
| } | ||
| keyB64Builder.append(l); | ||
| } | ||
| } | ||
| throw new IllegalArgumentException("invalid public key pem"); | ||
| } | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
.../java/dev/responsive/kafka/internal/license/exception/LicenseAuthenticationException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license.exception; | ||
|
|
||
| public class LicenseAuthenticationException extends LicenseException { | ||
| private static final long serialVersionUID = 0L; | ||
|
|
||
| public LicenseAuthenticationException(final String message) { | ||
| super(message); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
...lient/src/main/java/dev/responsive/kafka/internal/license/exception/LicenseException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license.exception; | ||
|
|
||
| public class LicenseException extends RuntimeException { | ||
| private static final long serialVersionUID = 0L; | ||
|
|
||
| public LicenseException(final String message) { | ||
| super(message); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
...in/java/dev/responsive/kafka/internal/license/exception/LicenseUseViolationException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license.exception; | ||
|
|
||
| public class LicenseUseViolationException extends LicenseException { | ||
| private static final long serialVersionUID = 0L; | ||
|
|
||
| public LicenseUseViolationException(final String message) { | ||
| super(message); | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
kafka-client/src/main/java/dev/responsive/kafka/internal/license/model/LicenseDocument.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license.model; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.annotation.JsonSubTypes; | ||
| import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||
|
|
||
| @JsonTypeInfo( | ||
| use = JsonTypeInfo.Id.NAME, | ||
| include = JsonTypeInfo.As.EXISTING_PROPERTY, | ||
| property = "version", | ||
| visible = true | ||
| ) | ||
| @JsonSubTypes({ | ||
| @JsonSubTypes.Type(value = LicenseDocumentV1.class, name = "1") | ||
| }) | ||
| public abstract class LicenseDocument { | ||
| private final String version; | ||
|
|
||
| @JsonCreator | ||
| public LicenseDocument(@JsonProperty("version") final String version) { | ||
| this.version = version; | ||
| } | ||
| } |
69 changes: 69 additions & 0 deletions
69
...a-client/src/main/java/dev/responsive/kafka/internal/license/model/LicenseDocumentV1.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /* | ||
| * Copyright 2024 Responsive Computing, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.responsive.kafka.internal.license.model; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import java.util.Base64; | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public class LicenseDocumentV1 extends LicenseDocument { | ||
| private final String info; | ||
| private final String signature; | ||
| private final String key; | ||
| private final String algo; | ||
|
|
||
| @JsonCreator | ||
| public LicenseDocumentV1( | ||
| @JsonProperty("version") final String version, | ||
| @JsonProperty("info") final String info, | ||
| @JsonProperty("signature") final String signature, | ||
| @JsonProperty("key") final String key, | ||
| @JsonProperty("algo") final String algo | ||
| ) { | ||
| super(version); | ||
| this.info = info; | ||
| this.signature = signature; | ||
| this.key = key; | ||
| this.algo = algo; | ||
| } | ||
|
|
||
| public String info() { | ||
| return info; | ||
| } | ||
|
|
||
| public String key() { | ||
| return key; | ||
| } | ||
|
|
||
| public String signature() { | ||
| return signature; | ||
| } | ||
|
|
||
| public String algo() { | ||
| return algo; | ||
| } | ||
|
|
||
| public byte[] decodeInfo() { | ||
| return Base64.getDecoder().decode(info); | ||
| } | ||
|
|
||
| public byte[] decodeSignature() { | ||
| return Base64.getDecoder().decode(signature); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking about what attack vectors there might be. I'm not at all worried about any attack vector for timed trial (should I be?), but when we start adding more complicated ones (i.e. usage based) the bad types of attacks open up (like an organization trying to use up credits for another organization by spoofing some part of this).
For that, doing any kind of client-side validation won't be possible since you could always attach a debugger and change things after the signature is verified.
I don't think this has any bearing on this PR, but we'll probably need to attach some kind of cryptographic license identifier to any metric we send home -- so just something to keep in mind to see if that changes the design here for anything.