-
Notifications
You must be signed in to change notification settings - Fork 9.2k
HDDS-1043. Enable token based authentication for S3 api. #561
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,5 +185,6 @@ public enum ResultCodes { | |
|
|
||
| INVALID_KMS_PROVIDER, | ||
|
|
||
| TOKEN_CREATION_ERROR | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you 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 org.apache.hadoop.ozone.security; | ||
|
|
||
| import org.apache.hadoop.util.StringUtils; | ||
| import org.apache.kerby.util.Hex; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import javax.crypto.Mac; | ||
| import javax.crypto.spec.SecretKeySpec; | ||
| import java.io.UnsupportedEncodingException; | ||
| import java.net.URLDecoder; | ||
| import java.nio.charset.Charset; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.security.GeneralSecurityException; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
|
|
||
| /** | ||
| * AWS v4 authentication payload validator. For more details refer to AWS | ||
| * documentation https://docs.aws.amazon.com/general/latest/gr/ | ||
| * sigv4-create-canonical-request.html. | ||
| **/ | ||
| final class AWSV4AuthValidator { | ||
|
|
||
| private final static Logger LOG = | ||
| LoggerFactory.getLogger(AWSV4AuthValidator.class); | ||
| private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256"; | ||
| private static final Charset UTF_8 = Charset.forName("utf-8"); | ||
|
|
||
| private AWSV4AuthValidator() { | ||
| } | ||
|
|
||
| private static String urlDecode(String str) { | ||
| try { | ||
| return URLDecoder.decode(str, UTF_8.name()); | ||
| } catch (UnsupportedEncodingException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| public static String hash(String payload) throws NoSuchAlgorithmException { | ||
| MessageDigest md = MessageDigest.getInstance("SHA-256"); | ||
| md.update(payload.getBytes(UTF_8)); | ||
| return String.format("%064x", new java.math.BigInteger(1, md.digest())); | ||
| } | ||
|
|
||
| private static byte[] sign(byte[] key, String msg) { | ||
| try { | ||
| SecretKeySpec signingKey = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM); | ||
| Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); | ||
| mac.init(signingKey); | ||
| return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); | ||
| } catch (GeneralSecurityException gse) { | ||
| throw new RuntimeException(gse); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns signing key. | ||
| * | ||
| * @param key | ||
| * @param strToSign | ||
| * | ||
| * SignatureKey = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + | ||
| * "<YourSecretAccessKey>","20130524"),"us-east-1"),"s3"),"aws4_request") | ||
| * | ||
| * For more details refer to AWS documentation: https://docs.aws.amazon | ||
| * .com/AmazonS3/latest/API/sig-v4-header-based-auth.html | ||
| * | ||
| * */ | ||
| private static byte[] getSigningKey(String key, String strToSign) { | ||
| String[] signData = StringUtils.split(StringUtils.split(strToSign, | ||
| '\n')[2], '/'); | ||
| String dateStamp = signData[0]; | ||
| String regionName = signData[1]; | ||
| String serviceName = signData[2]; | ||
| byte[] kDate = sign(("AWS4" + key).getBytes(UTF_8), dateStamp); | ||
| byte[] kRegion = sign(kDate, regionName); | ||
| byte[] kService = sign(kRegion, serviceName); | ||
| byte[] kSigning = sign(kService, "aws4_request"); | ||
| LOG.info(Hex.encode(kSigning)); | ||
| return kSigning; | ||
| } | ||
|
|
||
| /** | ||
| * Validate request by comparing Signature from request. Returns true if | ||
| * aws request is legit else returns false. | ||
| * Signature = HEX(HMAC_SHA256(key, String to Sign)) | ||
| * | ||
| * For more details refer to AWS documentation: https://docs.aws.amazon.com | ||
| * /AmazonS3/latest/API/sigv4-streaming.html | ||
| */ | ||
| public static boolean validateRequest(String strToSign, String signature, | ||
| String userKey) { | ||
| String expectedSignature = Hex.encode(sign(getSigningKey(userKey, | ||
| strToSign), strToSign)); | ||
| return expectedSignature.equals(signature); | ||
| } | ||
ajayydv marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| import org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient; | ||
| import org.apache.hadoop.hdds.security.x509.exceptions.CertificateException; | ||
| import org.apache.hadoop.io.Text; | ||
| import org.apache.hadoop.ozone.om.S3SecretManager; | ||
| import org.apache.hadoop.ozone.om.exceptions.OMException; | ||
| import org.apache.hadoop.ozone.security.OzoneSecretStore.OzoneManagerSecretState; | ||
| import org.apache.hadoop.ozone.security.OzoneTokenIdentifier.TokenInfo; | ||
|
|
@@ -43,7 +44,9 @@ | |
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| import static java.nio.charset.StandardCharsets.UTF_8; | ||
| import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_EXPIRED; | ||
| import static org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3TOKEN; | ||
|
|
||
| /** | ||
| * SecretManager for Ozone Master. Responsible for signing identifiers with | ||
|
|
@@ -58,6 +61,7 @@ public class OzoneDelegationTokenSecretManager | |
| .getLogger(OzoneDelegationTokenSecretManager.class); | ||
| private final Map<OzoneTokenIdentifier, TokenInfo> currentTokens; | ||
| private final OzoneSecretStore store; | ||
| private final S3SecretManager s3SecretManager; | ||
| private Thread tokenRemoverThread; | ||
| private final long tokenRemoverScanInterval; | ||
| private String omCertificateSerialId; | ||
|
|
@@ -80,12 +84,14 @@ public class OzoneDelegationTokenSecretManager | |
| */ | ||
| public OzoneDelegationTokenSecretManager(OzoneConfiguration conf, | ||
| long tokenMaxLifetime, long tokenRenewInterval, | ||
| long dtRemoverScanInterval, Text service) throws IOException { | ||
| long dtRemoverScanInterval, Text service, | ||
| S3SecretManager s3SecretManager) throws IOException { | ||
| super(new SecurityConfig(conf), tokenMaxLifetime, tokenRenewInterval, | ||
| service, LOG); | ||
| currentTokens = new ConcurrentHashMap(); | ||
| this.tokenRemoverScanInterval = dtRemoverScanInterval; | ||
| this.store = new OzoneSecretStore(conf); | ||
| this.s3SecretManager = s3SecretManager; | ||
| loadTokenSecretState(store.loadState()); | ||
| } | ||
|
|
||
|
|
@@ -279,14 +285,17 @@ public OzoneTokenIdentifier cancelToken(Token<OzoneTokenIdentifier> token, | |
| @Override | ||
| public byte[] retrievePassword(OzoneTokenIdentifier identifier) | ||
| throws InvalidToken { | ||
| if(identifier.getTokenType().equals(S3TOKEN)) { | ||
| return validateS3Token(identifier); | ||
| } | ||
| return validateToken(identifier).getPassword(); | ||
| } | ||
|
|
||
| /** | ||
| * Checks if TokenInfo for the given identifier exists in database and if the | ||
| * token is expired. | ||
| */ | ||
| public TokenInfo validateToken(OzoneTokenIdentifier identifier) | ||
| private TokenInfo validateToken(OzoneTokenIdentifier identifier) | ||
| throws InvalidToken { | ||
| TokenInfo info = currentTokens.get(identifier); | ||
| if (info == null) { | ||
|
|
@@ -327,6 +336,37 @@ public boolean verifySignature(OzoneTokenIdentifier identifier, | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates if a S3 identifier is valid or not. | ||
| * */ | ||
| private byte[] validateS3Token(OzoneTokenIdentifier identifier) | ||
| throws InvalidToken { | ||
| LOG.trace("Validating S3Token for identifier:{}", identifier); | ||
| String awsSecret; | ||
| try { | ||
| awsSecret = s3SecretManager.getS3UserSecretString(identifier | ||
| .getAwsAccessId()); | ||
| } catch (IOException e) { | ||
| LOG.error("Error while validating S3 identifier:{}", | ||
| identifier, e); | ||
| throw new InvalidToken("No S3 secret found for S3 identifier:" | ||
|
||
| + identifier); | ||
| } | ||
|
|
||
| if (awsSecret == null) { | ||
| throw new InvalidToken("No S3 secret found for S3 identifier:" | ||
| + identifier); | ||
| } | ||
|
|
||
| if (AWSV4AuthValidator.validateRequest(identifier.getStrToSign(), | ||
| identifier.getSignature(), awsSecret)) { | ||
| return identifier.getSignature().getBytes(UTF_8); | ||
| } | ||
| throw new InvalidToken("Invalid S3 identifier:" | ||
| + identifier); | ||
|
|
||
| } | ||
|
|
||
| // TODO: handle roll private key/certificate | ||
| private synchronized void removeExpiredKeys() { | ||
| long now = Time.now(); | ||
|
|
||
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.
The two methods are confusing a little. Especially as the bigger part of the implementation is duplicated. Would be great to merge them (or use better naming). (Not a blocker, we can address it 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.
Renamed new api to getS3UserSecretString, open to any better name you may suggest. Purpose of both api's is different so consolidating them right not might not be a good option. We can discuss this further in separate jira.