Skip to content

Commit

Permalink
Full support for AWS keypair/secrets & Refactoring for local secrets/…
Browse files Browse the repository at this point in the history
…keypair (#46)
  • Loading branch information
massenz authored Nov 21, 2022
1 parent 208703f commit 50a17bb
Show file tree
Hide file tree
Showing 44 changed files with 661 additions and 331 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Save both keys in a `private` folder (not under source control) and then point t
secrets:
keypair:
private: "private/ec-key-1.pem"
pub: "private/ec-key-pub.pem"
pub: "private/ec-key.pub"
```

You can use either an absolute path, or the relative path to the current directory from where you are launching the Web server.
Expand Down
24 changes: 4 additions & 20 deletions aws-upload-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,7 @@
# Copyright (c) 2021 AlertAvert.com. All rights reserved.
#
# 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.
#
# Author: Marco Massenzio (marco@alertavert.com)
#

#
# Generates a Elliptict Cryptography keypair using openssl

set -eu

Expand Down Expand Up @@ -72,15 +57,14 @@ out=$(mktemp /tmp/secret-XXXXXXXX.tmp)
cat <<EOF >$out
{
"priv": "$(while read -r line; do if [[ ! ${line} =~ ^----- ]]; \
then echo -n ${line}; fi; done < ${PRIV})",
"pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; done < ${PUB})",
"algorithm": "EC"
then echo -n ${line}; fi; done < ${PRIV})",
"pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; \
done < ${PUB})"
}
EOF


arn=$(aws ${ENDPOINT_URL} secretsmanager create-secret --name ${SECRET} \
--description "Keypair ${KEY} generated by the $(basename $0) script" \
--description "Elliptic Cryptography Keypair generated by the $(basename $0) script" \
--secret-string file://${out} | jq -r '.ARN')

rm $out
Expand Down
15 changes: 4 additions & 11 deletions jwt-opa/src/main/java/com/alertavert/opa/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* All constants are grouped here for ease of reference.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ExcludeFromCoverageGenerated
public class Constants {
/**
* Basic Authorization header type.
Expand All @@ -41,23 +42,12 @@ public class Constants {
*/
public static final String BEARER_TOKEN = "Bearer";

/**
* The type of encryption accepted by the {@link com.alertavert.opa.jwt.JwtTokenProvider}
*/
public static final String ELLIPTIC_CURVE = "EC";

/** Marker for a Public Key object */
public static final String PUBLIC_KEY = "PUBLIC KEY";

/** Marker for a Private Key object */
public static final String PRIVATE_KEY = "PRIVATE KEY";

/**
* Passphrase-based encryption (see
* {@link com.alertavert.opa.configuration.KeyMaterialConfiguration}.
*/
public static final String PASSPHRASE = "SECRET";

/**
* The name of the Env Var which contains the name of the file storing the AWS API Token in a
* running EKS container.
Expand Down Expand Up @@ -129,4 +119,7 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
};
public static final int MAX_TOKEN_LEN_LOG = 6;
public static final ObjectMapper MAPPER = new ObjectMapper();
public static final String PEM_EXT = ".pem";
public static final String PUB_EXT = ".pub";

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,58 +18,105 @@

package com.alertavert.opa.configuration;

import com.alertavert.opa.security.EnvSecretResolver;
import com.alertavert.opa.security.FileSecretResolver;
import com.alertavert.opa.security.NoopKeypairReader;
import com.alertavert.opa.security.NoopSecretResolver;
import com.alertavert.opa.security.SecretsResolver;
import com.alertavert.opa.security.aws.AwsSecretsKeypairReader;
import com.alertavert.opa.security.aws.AwsSecretsManagerResolver;
import com.alertavert.opa.security.crypto.KeyLoadException;
import com.alertavert.opa.security.crypto.KeypairFileReader;
import com.alertavert.opa.security.crypto.KeypairReader;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;

import java.io.IOException;
import javax.annotation.PostConstruct;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;

import static com.alertavert.opa.Constants.ELLIPTIC_CURVE;
import static com.alertavert.opa.Constants.PASSPHRASE;
import static com.alertavert.opa.Constants.PEM_EXT;
import static com.alertavert.opa.Constants.PUB_EXT;

@Slf4j
@Configuration
@EnableConfigurationProperties(TokensProperties.class)
@EnableConfigurationProperties({
KeysProperties.class,
TokensProperties.class
})
@RequiredArgsConstructor
public class KeyMaterialConfiguration {

private final TokensProperties tokensProperties;
private final KeysProperties keyProperties;

public KeyMaterialConfiguration(TokensProperties properties) {
this.tokensProperties = properties;
/**
* AWS SecretsManager client, will only be configured by the
* {@link com.alertavert.opa.security.aws.AwsClientConfiguration AWS Configuration} if the
* {@literal aws} profile is active.
*/
@Autowired(required = false)
SecretsManagerClient secretsManagerClient;

@PostConstruct
private void log() {
log.info("Configuring key material, algorithm = {}, location = {}",
keyProperties.algorithm, keyProperties.location);
}

@Bean
public String issuer() {
return tokensProperties.getIssuer();
SecretsResolver secretsResolver() {
return switch (keyProperties.getLocation()) {
case env -> new EnvSecretResolver();
case file -> new FileSecretResolver();
case keypair -> new NoopSecretResolver();
case awsSecret -> new AwsSecretsManagerResolver(secretsManagerClient);
case vaultPath -> throw new UnsupportedOperationException("Support for Vault not "
+ "implemented yet");
};
}

@Bean
Algorithm hmac(KeypairReader reader) {
switch (reader.algorithm()) {
case PASSPHRASE:
return Algorithm.HMAC256(tokensProperties.getSecret());
case ELLIPTIC_CURVE:
KeyPair keyPair = reader.loadKeys();
return Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(),
(ECPrivateKey) keyPair.getPrivate());
default:
throw new IllegalArgumentException(String.format("Algorithm [%s] not supported",
reader.algorithm()));
}
KeypairReader keypairReader() {
return switch (keyProperties.getLocation()) {
case env, file -> new NoopKeypairReader();
case keypair -> new KeypairFileReader(keyProperties.algorithm.name(),
Path.of(keyProperties.name + PEM_EXT), Path.of(keyProperties.name + PUB_EXT));
case awsSecret -> new AwsSecretsKeypairReader(secretsResolver(), keyProperties.name);
case vaultPath -> throw new UnsupportedOperationException("Support for Vault not "
+ "implemented yet");
};
}


@Bean
JWTVerifier verifier(Algorithm algorithm) throws IOException {
return JWT.require(algorithm)
.withIssuer(issuer())
.build();
Algorithm hmac(SecretsResolver resolver, KeypairReader reader) {
return switch (keyProperties.getAlgorithm()) {
case PASSPHRASE -> {
log.warn("Using insecure passphrase signing secret, name = {}", keyProperties.name);
String passphrase = resolver.getSecret(keyProperties.getName()).block();
if (passphrase == null) {
log.error("Could not resolve secret {}, with SecretsResolver {}",
keyProperties.name, resolver.getClass().getSimpleName());
throw new IllegalArgumentException("Signing secret cannot be resolved");
}
yield Algorithm.HMAC256(passphrase);
}
case EC -> {
KeyPair keyPair = reader.loadKeys().block();
if (keyPair == null) {
throw new KeyLoadException("Cannot load keypair " + keyProperties.name);
}
yield Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(),
(ECPrivateKey) keyPair.getPrivate());
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021 AlertAvert.com. All rights reserved.
*
* 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.
*
* Author: Marco Massenzio (marco@alertavert.com)
*/

package com.alertavert.opa.configuration;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* <h2>KeysPropertiers</h2>
*
* @author M. Massenzio, 202-11-19
*/
@Data
@ConfigurationProperties(prefix = "keys")
public class KeysProperties {

/**
* <h3>Location for the signing secret</h3>
*
* <p>Possible values are:
*
* <li>{@literal env}: only available for PASSPHRASE, env var name which contains
* the signing secret</li>
* <li>{@literal file}: only valid if algorithm is PASSPHRASE, the file is simply read</li>
* <li>{@literal keypair}: the filename without extension, to which `.pem` and `.pub`
* will be added</li>
* <li>{@literal aws_secret}: name of AWS SecretsManager secret</li>
* <li>{@literal vault_path}: path in HashiCorp Vault</li>
*
* <p>File paths can be absolute or relative.
*
* <p>For a {@link AlgorithmType#PASSPHRASE passphrase}, the secret is simply read from
* SecretsManager/Vault, the {@link AlgorithmType#EC keypair} keypair is
* stored as a JSON-formatted secret, with two keys: "priv" and "pub" (see
* {@link com.alertavert.opa.security.aws.AwsSecretsKeypairReader.SecretKeys}).
*/
enum KeyLocation {
env, file, keypair, awsSecret, vaultPath
}

public enum AlgorithmType {
/** Plaintext secret */
PASSPHRASE,
/** Elliptic Curve cryptography */
EC
}

AlgorithmType algorithm;
KeyLocation location;
String name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* <h2>KeyPropertiers</h2>
* <h2>TokensPropertiers</h2>
*
* @author M. Massenzio, 2020-12-14
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
import com.alertavert.opa.configuration.TokensProperties;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -56,15 +56,15 @@ public class JwtTokenProvider {
@Autowired
Algorithm hmac;

@Autowired
JWTVerifier verifier;

@Autowired
String issuer;

@Autowired
TokensProperties tokensProperties;

public JWTVerifier verifier() {
return JWT.require(hmac)
.withIssuer(tokensProperties.getIssuer())
.build();
}

/**
* <p>Creates a JWT token for the given user, signed with the private key of the issuer.
*
Expand All @@ -88,7 +88,7 @@ public String createToken(String user, List<String> roles, @Nullable Instant exp

log.debug("Issuing JWT for user = {}, roles = {}", user, roles);
JWTCreator.Builder builder = JWT.create()
.withIssuer(issuer)
.withIssuer(tokensProperties.getIssuer())
.withSubject(user)
.withIssuedAt(Date.from(now))
.withArrayClaim(ROLES, roles.toArray(new String[0]));
Expand Down Expand Up @@ -131,7 +131,7 @@ public boolean validateToken(String token) {
}

public DecodedJWT decode(String token) throws JWTVerificationException {
return verifier.verify(token);
return verifier().verify(token);
}

public Authentication getAuthentication(String token) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 AlertAvert.com. All rights reserved.
*
* 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.
*
* Author: Marco Massenzio (marco@alertavert.com)
*/

package com.alertavert.opa.security;

import reactor.core.publisher.Mono;

/**
* <H2>EnvSecretResolver</H2>
*
* <p>Reads a secret from an environment variable.
*
* <p>In all its triviality, this class is virtually untestable, due to
* the JVM limitations on setting test env vars (see {@literal EnvSecretReaderTest}).
*
* @author M. Massenzio, 2022-11-19
*/
public class EnvSecretResolver implements SecretsResolver {
@Override
public Mono<String> getSecret(String secretName) {
return Mono.justOrEmpty(System.getenv(secretName));
}
}
Loading

0 comments on commit 50a17bb

Please sign in to comment.