HDDS-804. Block token: Add secret token manager. Contributed by Ajay Kumar.

This commit is contained in:
Ajay Kumar 2018-11-29 08:00:41 -08:00 committed by Xiaoyu Yao
parent 0c8829a9a1
commit 6d522dc05d
8 changed files with 1183 additions and 445 deletions

View File

@ -21,6 +21,7 @@
import com.google.common.base.Preconditions;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -77,6 +78,7 @@ public class SecurityConfig {
private final Duration certDuration;
private final String x509SignatureAlgo;
private final Boolean grpcBlockTokenEnabled;
private final int getMaxKeyLength;
private final String certificateDir;
private final String certificateFileName;
@ -88,6 +90,9 @@ public class SecurityConfig {
public SecurityConfig(Configuration configuration) {
Preconditions.checkNotNull(configuration, "Configuration cannot be null");
this.configuration = configuration;
this.getMaxKeyLength = configuration.getInt(
OzoneConfigKeys.OZONE_MAX_KEY_LEN,
OzoneConfigKeys.OZONE_MAX_KEY_LEN_DEFAULT);
this.size = this.configuration.getInt(HDDS_KEY_LEN, HDDS_DEFAULT_KEY_LEN);
this.keyAlgo = this.configuration.get(HDDS_KEY_ALGORITHM,
HDDS_DEFAULT_KEY_ALGORITHM);
@ -289,4 +294,8 @@ private Provider initSecurityProvider(String providerName) {
throw new SecurityException("Unknown security provider:" + provider);
}
}
public int getMaxKeyLength() {
return this.getMaxKeyLength;
}
}

View File

@ -0,0 +1,191 @@
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos.BlockTokenSecretProto.AccessModeProto;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.KeyPair;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Map;
/**
* SecretManager for Ozone Master block tokens.
*/
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class OzoneBlockTokenSecretManager extends
OzoneSecretManager<OzoneBlockTokenIdentifier> {
private static final Logger LOG = LoggerFactory
.getLogger(OzoneBlockTokenSecretManager.class);;
// Will be set by grpc clients for individual datanodes.
static final Text SERVICE = new Text("HDDS_SERVICE");
private final String omCertSerialId;
/**
* Create a secret manager.
*
* @param conf
* @param blockTokenExpirytime token expiry time for expired tokens in
* milliseconds
*/
public OzoneBlockTokenSecretManager(OzoneConfiguration conf,
long blockTokenExpirytime, String omCertSerialId) {
super(conf, blockTokenExpirytime, blockTokenExpirytime, SERVICE, LOG);
this.omCertSerialId = omCertSerialId;
}
@Override
public OzoneBlockTokenIdentifier createIdentifier() {
throw new SecurityException("Ozone block token can't be created "
+ "without owner and access mode information.");
}
public OzoneBlockTokenIdentifier createIdentifier(String owner,
String blockId, EnumSet<AccessModeProto> modes, long maxLength) {
return new OzoneBlockTokenIdentifier(owner, blockId, modes,
getTokenExpiryTime(), omCertSerialId, maxLength);
}
/**
* Generate an block token for specified user, blockId.
*
* @param user
* @param blockId
* @param modes
* @param maxLength
* @return token
*/
public Token<OzoneBlockTokenIdentifier> generateToken(String user,
String blockId, EnumSet<AccessModeProto> modes, long maxLength) {
OzoneBlockTokenIdentifier tokenIdentifier = createIdentifier(user,
blockId, modes, maxLength);
if (LOG.isTraceEnabled()) {
long expiryTime = tokenIdentifier.getExpiryDate();
String tokenId = tokenIdentifier.toString();
LOG.trace("Issued delegation token -> expiryTime:{},tokenId:{}",
expiryTime, tokenId);
}
return new Token<>(tokenIdentifier.getBytes(),
createPassword(tokenIdentifier), tokenIdentifier.getKind(), SERVICE);
}
/**
* Generate an block token for current user.
*
* @param blockId
* @param modes
* @return token
*/
public Token<OzoneBlockTokenIdentifier> generateToken(String blockId,
EnumSet<AccessModeProto> modes, long maxLength) throws IOException {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
String userID = (ugi == null ? null : ugi.getShortUserName());
return generateToken(userID, blockId, modes, maxLength);
}
@Override
public byte[] retrievePassword(OzoneBlockTokenIdentifier identifier)
throws InvalidToken {
validateToken(identifier);
return createPassword(identifier);
}
@Override
public long renewToken(Token<OzoneBlockTokenIdentifier> token,
String renewer) throws IOException {
throw new UnsupportedOperationException("Renew token operation is not " +
"supported for ozone block tokens.");
}
@Override
public OzoneBlockTokenIdentifier cancelToken(Token<OzoneBlockTokenIdentifier>
token, String canceller) throws IOException {
throw new UnsupportedOperationException("Cancel token operation is not " +
"supported for ozone block tokens.");
}
/**
* Find the OzoneBlockTokenInfo for the given token id, and verify that if the
* token is not expired.
*/
public boolean validateToken(OzoneBlockTokenIdentifier identifier)
throws InvalidToken {
long now = Time.now();
if (identifier.getExpiryDate() < now) {
throw new InvalidToken("token " + formatTokenId(identifier) + " is " +
"expired, current time: " + Time.formatTime(now) +
" expiry time: " + identifier.getExpiryDate());
}
if (!verifySignature(identifier, createPassword(identifier))) {
throw new InvalidToken("Tampared/Inavalid token.");
}
return true;
}
/**
* Should be called before this object is used.
*/
@Override
public synchronized void start(KeyPair keyPair) throws IOException {
super.start(keyPair);
removeExpiredKeys();
}
/**
* Returns expiry time by adding configured expiry time with current time.
*
* @return Expiry time.
*/
private long getTokenExpiryTime() {
return Time.now() + getTokenRenewInterval();
}
/**
* Should be called before this object is used.
*/
@Override
public synchronized void stop() throws IOException {
super.stop();
}
private synchronized void removeExpiredKeys() {
// TODO: handle roll private key/certificate
long now = Time.now();
for (Iterator<Map.Entry<Integer, OzoneSecretKey>> it = allKeys.entrySet()
.iterator(); it.hasNext();) {
Map.Entry<Integer, OzoneSecretKey> e = it.next();
OzoneSecretKey key = e.getValue();
if (key.getExpiryDate() < now) {
it.remove();
}
}
}
}

View File

@ -0,0 +1,455 @@
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.ozone.security.OzoneSecretStore.OzoneManagerSecretState;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier.TokenInfo;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.HadoopKerberosName;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.Daemon;
import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* SecretManager for Ozone Master. Responsible for signing identifiers with
* private key,
*/
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class OzoneDelegationTokenSecretManager<T extends OzoneTokenIdentifier>
extends OzoneSecretManager<T> {
private static final Logger LOG = LoggerFactory
.getLogger(OzoneDelegationTokenSecretManager.class);
private final Map<T, TokenInfo> currentTokens;
private final OzoneSecretStore store;
private Thread tokenRemoverThread;
private final long tokenRemoverScanInterval;
/**
* If the delegation token update thread holds this lock, it will not get
* interrupted.
*/
private Object noInterruptsLock = new Object();
/**
* Create a secret manager.
*
* @param conf configuration.
* @param tokenMaxLifetime the maximum lifetime of the delegation tokens in
* milliseconds
* @param tokenRenewInterval how often the tokens must be renewed in
* milliseconds
* @param dtRemoverScanInterval how often the tokens are scanned for expired
* tokens in milliseconds
*/
public OzoneDelegationTokenSecretManager(OzoneConfiguration conf,
long tokenMaxLifetime, long tokenRenewInterval,
long dtRemoverScanInterval, Text service) throws IOException {
super(conf, tokenMaxLifetime, tokenRenewInterval, service, LOG);
currentTokens = new ConcurrentHashMap();
this.tokenRemoverScanInterval = dtRemoverScanInterval;
this.store = new OzoneSecretStore(conf);
loadTokenSecretState(store.loadState());
}
@Override
public T createIdentifier() {
return (T) T.newInstance();
}
/**
* Create new Identifier with given,owner,renwer and realUser.
*
* @return T
*/
public T createIdentifier(Text owner, Text renewer, Text realUser) {
return (T) T.newInstance(owner, renewer, realUser);
}
/**
* Returns {@link Token} for given identifier.
*
* @param owner
* @param renewer
* @param realUser
* @return Token
* @throws IOException to allow future exceptions to be added without breaking
* compatibility
*/
public Token<T> createToken(Text owner, Text renewer, Text realUser)
throws IOException {
T identifier = createIdentifier(owner, renewer, realUser);
updateIdentifierDetails(identifier);
byte[] password = createPassword(identifier.getBytes(),
getCurrentKey().getPrivateKey());
addToTokenStore(identifier, password);
Token<T> token = new Token<>(identifier.getBytes(), password,
identifier.getKind(), getService());
if (LOG.isTraceEnabled()) {
long expiryTime = identifier.getIssueDate() + getTokenRenewInterval();
String tokenId = identifier.toStringStable();
LOG.trace("Issued delegation token -> expiryTime:{},tokenId:{}",
expiryTime, tokenId);
}
return token;
}
/**
* Stores given identifier in token store.
*
* @param identifier
* @param password
* @throws IOException
*/
private void addToTokenStore(T identifier, byte[] password)
throws IOException {
TokenInfo tokenInfo = new TokenInfo(identifier.getIssueDate()
+ getTokenRenewInterval(), password, identifier.getTrackingId());
currentTokens.put(identifier, tokenInfo);
store.storeToken(identifier, tokenInfo.getRenewDate());
}
/**
* Updates issue date, master key id and sequence number for identifier.
*
* @param identifier the identifier to validate
*/
private void updateIdentifierDetails(T identifier) {
int sequenceNum;
long now = Time.monotonicNow();
sequenceNum = incrementDelegationTokenSeqNum();
identifier.setIssueDate(now);
identifier.setMasterKeyId(getCurrentKey().getKeyId());
identifier.setSequenceNumber(sequenceNum);
identifier.setMaxDate(Time.monotonicNow() + getTokenMaxLifetime());
}
/**
* Renew a delegation token.
*
* @param token the token to renew
* @param renewer the full principal name of the user doing the renewal
* @return the new expiration time
* @throws InvalidToken if the token is invalid
* @throws AccessControlException if the user can't renew token
*/
@Override
public synchronized long renewToken(Token<T> token, String renewer)
throws IOException {
ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier());
DataInputStream in = new DataInputStream(buf);
T id = (T) T.readProtoBuf(in);
if(LOG.isDebugEnabled()) {
LOG.debug("Token renewal for identifier: {}, total currentTokens: {}",
formatTokenId(id), currentTokens.size());
}
long now = Time.monotonicNow();
if (id.getMaxDate() < now) {
throw new InvalidToken(renewer + " tried to renew an expired token "
+ formatTokenId(id) + " max expiration date: "
+ Time.formatTime(id.getMaxDate())
+ " currentTime: " + Time.formatTime(now));
}
validateToken(id);
if ((id.getRenewer() == null) || (id.getRenewer().toString().isEmpty())) {
throw new AccessControlException(renewer +
" tried to renew a token " + formatTokenId(id)
+ " without a renewer");
}
if (!id.getRenewer().toString().equals(renewer)) {
throw new AccessControlException(renewer
+ " tries to renew a token " + formatTokenId(id)
+ " with non-matching renewer " + id.getRenewer());
}
OzoneSecretKey key = allKeys.get(id.getMasterKeyId());
if (key == null) {
throw new InvalidToken("Unable to find master key for keyId="
+ id.getMasterKeyId()
+ " from cache. Failed to renew an unexpired token "
+ formatTokenId(id) + " with sequenceNumber="
+ id.getSequenceNumber());
}
byte[] password = createPassword(token.getIdentifier(),
key.getPrivateKey());
long renewTime = Math.min(id.getMaxDate(), now + getTokenRenewInterval());
try {
addToTokenStore(id, password);
} catch (IOException e) {
LOG.error("Unable to update token " + id.getSequenceNumber(), e);
}
return renewTime;
}
/**
* Cancel a token by removing it from store and cache.
*
* @return Identifier of the canceled token
* @throws InvalidToken for invalid token
* @throws AccessControlException if the user isn't allowed to cancel
*/
public T cancelToken(Token<T> token, String canceller) throws IOException {
T id = (T) T.readProtoBuf(token.getIdentifier());
LOG.debug("Token cancellation requested for identifier: {}",
formatTokenId(id));
if (id.getUser() == null) {
throw new InvalidToken("Token with no owner " + formatTokenId(id));
}
String owner = id.getUser().getUserName();
Text renewer = id.getRenewer();
HadoopKerberosName cancelerKrbName = new HadoopKerberosName(canceller);
String cancelerShortName = cancelerKrbName.getShortName();
if (!canceller.equals(owner)
&& (renewer == null || renewer.toString().isEmpty()
|| !cancelerShortName
.equals(renewer.toString()))) {
throw new AccessControlException(canceller
+ " is not authorized to cancel the token " + formatTokenId(id));
}
try {
store.removeToken(id);
} catch (IOException e) {
LOG.error("Unable to remove token " + id.getSequenceNumber(), e);
}
TokenInfo info = currentTokens.remove(id);
if (info == null) {
throw new InvalidToken("Token not found " + formatTokenId(id));
}
return id;
}
@Override
public byte[] retrievePassword(T identifier) throws InvalidToken {
return validateToken(identifier).getPassword();
}
/**
* Checks if TokenInfo for the given identifier exists in database and if the
* token is expired.
*/
public TokenInfo validateToken(T identifier) throws InvalidToken {
TokenInfo info = currentTokens.get(identifier);
if (info == null) {
throw new InvalidToken("token " + formatTokenId(identifier)
+ " can't be found in cache");
}
long now = Time.monotonicNow();
if (info.getRenewDate() < now) {
throw new InvalidToken("token " + formatTokenId(identifier) + " is " +
"expired, current time: " + Time.formatTime(now) +
" expected renewal time: " + Time.formatTime(info.getRenewDate()));
}
if (!verifySignature(identifier, info.getPassword())) {
throw new InvalidToken("Tampared/Inavalid token.");
}
return info;
}
// TODO: handle roll private key/certificate
private synchronized void removeExpiredKeys() {
long now = Time.monotonicNow();
for (Iterator<Map.Entry<Integer, OzoneSecretKey>> it = allKeys.entrySet()
.iterator(); it.hasNext();) {
Map.Entry<Integer, OzoneSecretKey> e = it.next();
OzoneSecretKey key = e.getValue();
if (key.getExpiryDate() < now && key.getExpiryDate() != -1) {
if (!key.equals(getCurrentKey())) {
it.remove();
try {
store.removeTokenMasterKey(key);
} catch (IOException ex) {
LOG.error("Unable to remove master key " + key.getKeyId(), ex);
}
}
}
}
}
private void loadTokenSecretState(OzoneManagerSecretState<T> state)
throws IOException {
LOG.info("Loading token state into token manager.");
for (OzoneSecretKey key : state.ozoneManagerSecretState()) {
allKeys.putIfAbsent(key.getKeyId(), key);
incrementCurrentKeyId();
}
for (Map.Entry<T, Long> entry : state.getTokenState().entrySet()) {
addPersistedDelegationToken(entry.getKey(), entry.getValue());
}
}
private void addPersistedDelegationToken(
T identifier, long renewDate)
throws IOException {
if (isRunning()) {
// a safety check
throw new IOException(
"Can't add persisted delegation token to a running SecretManager.");
}
int keyId = identifier.getMasterKeyId();
OzoneSecretKey dKey = allKeys.get(keyId);
if (dKey == null) {
LOG.warn("No KEY found for persisted identifier "
+ formatTokenId(identifier));
return;
}
PrivateKey privateKey = dKey.getPrivateKey();
byte[] password = createPassword(identifier.getBytes(), privateKey);
if (identifier.getSequenceNumber() > getDelegationTokenSeqNum()) {
setDelegationTokenSeqNum(identifier.getSequenceNumber());
}
if (currentTokens.get(identifier) == null) {
currentTokens.put(identifier, new TokenInfo(renewDate,
password, identifier.getTrackingId()));
} else {
throw new IOException("Same delegation token being added twice: "
+ formatTokenId(identifier));
}
}
/**
* Should be called before this object is used.
*/
@Override
public synchronized void start(KeyPair keyPair) throws IOException {
super.start(keyPair);
storeKey(getCurrentKey());
removeExpiredKeys();
tokenRemoverThread = new Daemon(new ExpiredTokenRemover());
tokenRemoverThread.start();
}
private void storeKey(OzoneSecretKey key) throws IOException {
store.storeTokenMasterKey(key);
if (!allKeys.containsKey(key.getKeyId())) {
allKeys.put(key.getKeyId(), key);
}
}
public void stopThreads() {
if (LOG.isDebugEnabled()) {
LOG.debug("Stopping expired delegation token remover thread");
}
setIsRunning(false);
if (tokenRemoverThread != null) {
synchronized (noInterruptsLock) {
tokenRemoverThread.interrupt();
}
try {
tokenRemoverThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(
"Unable to join on token removal thread", e);
}
}
}
/**
* Stops the OzoneDelegationTokenSecretManager.
*
* @throws IOException
*/
@Override
public void stop() throws IOException {
super.stop();
stopThreads();
if (this.store != null) {
this.store.close();
}
}
/**
* Remove expired delegation tokens from cache and persisted store.
*/
private void removeExpiredToken() {
long now = Time.monotonicNow();
synchronized (this) {
Iterator<Map.Entry<T,
TokenInfo>> i = currentTokens.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<T,
TokenInfo> entry = i.next();
long renewDate = entry.getValue().getRenewDate();
if (renewDate < now) {
i.remove();
try {
store.removeToken(entry.getKey());
} catch (IOException e) {
if(LOG.isDebugEnabled()) {
LOG.debug("Failed to remove expired token {}", entry.getValue());
}
}
}
}
}
}
private class ExpiredTokenRemover extends Thread {
private long lastTokenCacheCleanup;
@Override
public void run() {
LOG.info("Starting expired delegation token remover thread, "
+ "tokenRemoverScanInterval=" + getTokenRemoverScanInterval()
/ (60 * 1000) + " min(s)");
try {
while (isRunning()) {
long now = Time.monotonicNow();
if (lastTokenCacheCleanup + getTokenRemoverScanInterval()
< now) {
removeExpiredToken();
lastTokenCacheCleanup = now;
}
try {
Thread.sleep(Math.min(5000,
getTokenRemoverScanInterval())); // 5 seconds
} catch (InterruptedException ie) {
LOG.error("ExpiredTokenRemover received " + ie);
}
}
} catch (Throwable t) {
LOG.error("ExpiredTokenRemover thread received unexpected exception",
t);
Runtime.getRuntime().exit(-1);
}
}
}
public long getTokenRemoverScanInterval() {
return tokenRemoverScanInterval;
}
}

View File

@ -18,8 +18,17 @@
package org.apache.hadoop.ozone.security;
import com.google.common.base.Preconditions;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.security.x509.SecurityConfig;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.token.SecretManager;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.slf4j.Logger;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
@ -27,25 +36,9 @@
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.security.OzoneSecretStore.OzoneManagerSecretState;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier.TokenInfo;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.HadoopKerberosName;
import org.apache.hadoop.security.token.SecretManager;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.Daemon;
import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SecretManager for Ozone Master. Responsible for signing identifiers with
@ -53,33 +46,23 @@
*/
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class OzoneSecretManager<T extends OzoneTokenIdentifier>
public abstract class OzoneSecretManager<T extends TokenIdentifier>
extends SecretManager<T> {
private static final Logger LOG = LoggerFactory
.getLogger(OzoneSecretManager.class);
private final Logger logger;
/**
* The name of the Private/Public Key based hashing algorithm.
*/
private static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withRSA";
private final SecurityConfig securityConfig;
private final long tokenMaxLifetime;
private final long tokenRenewInterval;
private final long tokenRemoverScanInterval;
private final Text service;
private final Map<Integer, OzoneSecretKey> allKeys;
private final Map<T, TokenInfo> currentTokens;
private final OzoneSecretStore store;
private Thread tokenRemoverThread;
private volatile boolean running;
private AtomicInteger tokenSequenceNumber;
private OzoneSecretKey currentKey;
private AtomicInteger currentKeyId;
/**
* If the delegation token update thread holds this lock, it will not get
* interrupted.
*/
private Object noInterruptsLock = new Object();
private int maxKeyLength;
private AtomicInteger currentKeyId;
private AtomicInteger tokenSequenceNumber;
protected final Map<Integer, OzoneSecretKey> allKeys;
/**
* Create a secret manager.
@ -89,100 +72,21 @@ public class OzoneSecretManager<T extends OzoneTokenIdentifier>
* milliseconds
* @param tokenRenewInterval how often the tokens must be renewed in
* milliseconds
* @param dtRemoverScanInterval how often the tokens are scanned for expired
* tokens in milliseconds
* @param service name of service
*/
public OzoneSecretManager(OzoneConfiguration conf, long tokenMaxLifetime,
long tokenRenewInterval, long dtRemoverScanInterval, Text service)
throws IOException {
long tokenRenewInterval, Text service, Logger logger) {
this.securityConfig = new SecurityConfig(conf);
this.tokenMaxLifetime = tokenMaxLifetime;
this.tokenRenewInterval = tokenRenewInterval;
this.tokenRemoverScanInterval = dtRemoverScanInterval;
currentTokens = new ConcurrentHashMap();
allKeys = new ConcurrentHashMap<>();
currentKeyId = new AtomicInteger();
tokenSequenceNumber = new AtomicInteger();
this.store = new OzoneSecretStore(conf);
loadTokenSecretState(store.loadState());
allKeys = new ConcurrentHashMap<>();
this.service = service;
this.maxKeyLength = conf.getInt(OzoneConfigKeys.OZONE_MAX_KEY_LEN,
OzoneConfigKeys.OZONE_MAX_KEY_LEN_DEFAULT);
this.maxKeyLength = securityConfig.getMaxKeyLength();
this.logger = logger;
}
@Override
public T createIdentifier() {
return (T) T.newInstance();
}
/**
* Create new Identifier with given,owner,renwer and realUser.
*
* @return T
*/
public T createIdentifier(Text owner, Text renewer, Text realUser) {
return (T) T.newInstance(owner, renewer, realUser);
}
/**
* Returns {@link Token} for given identifier.
*
* @param owner
* @param renewer
* @param realUser
* @return Token
* @throws IOException to allow future exceptions to be added without breaking
* compatibility
*/
public Token<T> createToken(Text owner, Text renewer, Text realUser)
throws IOException {
T identifier = createIdentifier(owner, renewer, realUser);
updateIdentifierDetails(identifier);
byte[] password = createPassword(identifier.getBytes(),
currentKey.getPrivateKey());
addToTokenStore(identifier, password);
Token<T> token = new Token<>(identifier.getBytes(), password,
identifier.getKind(), service);
if (LOG.isTraceEnabled()) {
long expiryTime = identifier.getIssueDate() + tokenRenewInterval;
String tokenId = identifier.toStringStable();
LOG.trace("Issued delegation token -> expiryTime:{},tokenId:{}",
expiryTime, tokenId);
}
return token;
}
/**
* Stores given identifier in token store.
*
* @param identifier
* @param password
* @throws IOException
*/
private void addToTokenStore(T identifier, byte[] password)
throws IOException {
TokenInfo tokenInfo = new TokenInfo(identifier.getIssueDate()
+ tokenRenewInterval, password, identifier.getTrackingId());
currentTokens.put(identifier, tokenInfo);
store.storeToken(identifier, tokenInfo.getRenewDate());
}
/**
* Updates issue date, master key id and sequence number for identifier.
*
* @param identifier the identifier to validate
*/
private void updateIdentifierDetails(T identifier) {
int sequenceNum;
long now = Time.monotonicNow();
sequenceNum = incrementDelegationTokenSeqNum();
identifier.setIssueDate(now);
identifier.setMasterKeyId(currentKey.getKeyId());
identifier.setSequenceNumber(sequenceNum);
identifier.setMaxDate(Time.monotonicNow() + tokenMaxLifetime);
}
/**
* Compute HMAC of the identifier using the private key and return the output
@ -196,7 +100,7 @@ public byte[] createPassword(byte[] identifier, PrivateKey privateKey)
throws OzoneSecurityException {
try {
Signature rsaSignature = Signature.getInstance(
DEFAULT_SIGNATURE_ALGORITHM);
getDefaultSignatureAlgorithm());
rsaSignature.initSign(privateKey);
rsaSignature.update(identifier);
return rsaSignature.sign();
@ -210,22 +114,31 @@ public byte[] createPassword(byte[] identifier, PrivateKey privateKey)
@Override
public byte[] createPassword(T identifier) {
LOG.debug("Creating password for identifier: {}, currentKey: {}",
logger.debug("Creating password for identifier: {}, currentKey: {}",
formatTokenId(identifier), currentKey.getKeyId());
byte[] password = null;
try {
password = createPassword(identifier.getBytes(),
currentKey.getPrivateKey());
} catch (IOException ioe) {
LOG.error("Could not store token {}!!", formatTokenId(identifier),
logger.error("Could not store token {}!!", formatTokenId(identifier),
ioe);
}
return password;
}
/**
* Default implementation for Ozone. Verifies if hash in token is legit.
* */
@Override
public byte[] retrievePassword(T identifier) throws InvalidToken {
return checkToken(identifier).getPassword();
byte[] password = createPassword(identifier);
// TODO: Revisit this when key/certificate rotation is implemented.
// i.e Try all valid keys instead of current key only.
if (!verifySignature(identifier, password)) {
throw new InvalidToken("Tampared/Inavalid token.");
}
return password;
}
/**
@ -237,52 +150,8 @@ public byte[] retrievePassword(T identifier) throws InvalidToken {
* @throws InvalidToken if the token is invalid
* @throws AccessControlException if the user can't renew token
*/
public synchronized long renewToken(Token<T> token, String renewer)
throws IOException {
ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier());
DataInputStream in = new DataInputStream(buf);
T id = (T) T.readProtoBuf(in);
LOG.debug("Token renewal for identifier: {}, total currentTokens: {}",
formatTokenId(id), currentTokens.size());
long now = Time.monotonicNow();
if (id.getMaxDate() < now) {
throw new InvalidToken(renewer + " tried to renew an expired token "
+ formatTokenId(id) + " max expiration date: "
+ Time.formatTime(id.getMaxDate())
+ " currentTime: " + Time.formatTime(now));
}
checkToken(id);
if ((id.getRenewer() == null) || (id.getRenewer().toString().isEmpty())) {
throw new AccessControlException(renewer +
" tried to renew a token " + formatTokenId(id)
+ " without a renewer");
}
if (!id.getRenewer().toString().equals(renewer)) {
throw new AccessControlException(renewer
+ " tries to renew a token " + formatTokenId(id)
+ " with non-matching renewer " + id.getRenewer());
}
OzoneSecretKey key = allKeys.get(id.getMasterKeyId());
if (key == null) {
throw new InvalidToken("Unable to find master key for keyId="
+ id.getMasterKeyId()
+ " from cache. Failed to renew an unexpired token "
+ formatTokenId(id) + " with sequenceNumber="
+ id.getSequenceNumber());
}
byte[] password = createPassword(token.getIdentifier(),
key.getPrivateKey());
long renewTime = Math.min(id.getMaxDate(), now + tokenRenewInterval);
try {
addToTokenStore(id, password);
} catch (IOException e) {
LOG.error("Unable to update token " + id.getSequenceNumber(), e);
}
return renewTime;
}
public abstract long renewToken(Token<T> token, String renewer)
throws IOException;
/**
* Cancel a token by removing it from store and cache.
*
@ -290,44 +159,8 @@ public synchronized long renewToken(Token<T> token, String renewer)
* @throws InvalidToken for invalid token
* @throws AccessControlException if the user isn't allowed to cancel
*/
public T cancelToken(Token<T> token, String canceller) throws IOException {
T id = (T) T.readProtoBuf(token.getIdentifier());
LOG.debug("Token cancellation requested for identifier: {}",
formatTokenId(id));
if (id.getUser() == null) {
throw new InvalidToken("Token with no owner " + formatTokenId(id));
}
String owner = id.getUser().getUserName();
Text renewer = id.getRenewer();
HadoopKerberosName cancelerKrbName = new HadoopKerberosName(canceller);
String cancelerShortName = cancelerKrbName.getShortName();
if (!canceller.equals(owner)
&& (renewer == null || renewer.toString().isEmpty()
|| !cancelerShortName
.equals(renewer.toString()))) {
throw new AccessControlException(canceller
+ " is not authorized to cancel the token " + formatTokenId(id));
}
try {
store.removeToken(id);
} catch (IOException e) {
LOG.error("Unable to remove token " + id.getSequenceNumber(), e);
}
TokenInfo info = currentTokens.remove(id);
if (info == null) {
throw new InvalidToken("Token not found " + formatTokenId(id));
}
return id;
}
public int getCurrentKeyId() {
return currentKeyId.get();
}
public void setCurrentKeyId(int keyId) {
currentKeyId.set(keyId);
}
public abstract T cancelToken(Token<T> token, String canceller)
throws IOException;
public int incrementCurrentKeyId() {
return currentKeyId.incrementAndGet();
@ -346,14 +179,31 @@ public int incrementDelegationTokenSeqNum() {
}
/**
* Validates if given token is valid.
* Update the current master key. This is called once by start method before
* tokenRemoverThread is created,
*/
private OzoneSecretKey updateCurrentKey(KeyPair keyPair) throws IOException {
logger.info("Updating the current master key for generating tokens");
// TODO: fix me based on the certificate expire time to set the key
// expire time.
int newCurrentId = incrementCurrentKeyId();
OzoneSecretKey newKey = new OzoneSecretKey(newCurrentId, -1,
keyPair, maxKeyLength);
currentKey = newKey;
return currentKey;
}
/**
* Validates if given hash is valid.
*
* @param identifier
* @param password
*/
private boolean validateToken(T identifier, byte[] password) {
public boolean verifySignature(T identifier, byte[] password) {
try {
Signature rsaSignature = Signature.getInstance("SHA256withRSA");
Signature rsaSignature =
Signature.getInstance(getDefaultSignatureAlgorithm());
rsaSignature.initVerify(currentKey.getPublicKey());
rsaSignature.update(identifier.getBytes());
return rsaSignature.verify(password);
@ -363,179 +213,45 @@ private boolean validateToken(T identifier, byte[] password) {
}
}
/**
* Checks if TokenInfo for the given identifier exists in database and if the
* token is expired.
*/
public TokenInfo checkToken(T identifier) throws InvalidToken {
TokenInfo info = currentTokens.get(identifier);
if (info == null) {
throw new InvalidToken("token " + formatTokenId(identifier)
+ " can't be found in cache");
}
long now = Time.monotonicNow();
if (info.getRenewDate() < now) {
throw new InvalidToken("token " + formatTokenId(identifier) + " is " +
"expired, current time: " + Time.formatTime(now) +
" expected renewal time: " + Time.formatTime(info.getRenewDate()));
}
if (!validateToken(identifier, info.getPassword())) {
throw new InvalidToken("Tampared/Inavalid token.");
}
return info;
}
// TODO: handle roll private key/certificate
private synchronized void removeExpiredKeys() {
long now = Time.monotonicNow();
for (Iterator<Map.Entry<Integer, OzoneSecretKey>> it = allKeys.entrySet()
.iterator(); it.hasNext();) {
Map.Entry<Integer, OzoneSecretKey> e = it.next();
OzoneSecretKey key = e.getValue();
if (key.getExpiryDate() < now && key.getExpiryDate() != -1) {
if (!key.equals(currentKey)) {
it.remove();
try {
store.removeTokenMasterKey(key);
} catch (IOException ex) {
LOG.error("Unable to remove master key " + key.getKeyId(), ex);
}
}
}
}
}
private void loadTokenSecretState(OzoneManagerSecretState<T> state)
throws IOException {
LOG.info("Loading token state into token manager.");
for (OzoneSecretKey key : state.ozoneManagerSecretState()) {
allKeys.putIfAbsent(key.getKeyId(), key);
}
for (Map.Entry<T, Long> entry : state.getTokenState().entrySet()) {
addPersistedDelegationToken(entry.getKey(), entry.getValue());
}
}
private String formatTokenId(T id) {
public String formatTokenId(T id) {
return "(" + id + ")";
}
private void addPersistedDelegationToken(
T identifier, long renewDate)
throws IOException {
if (running) {
// a safety check
throw new IOException(
"Can't add persisted delegation token to a running SecretManager.");
}
int keyId = identifier.getMasterKeyId();
OzoneSecretKey dKey = allKeys.get(keyId);
if (dKey == null) {
LOG.warn("No KEY found for persisted identifier "
+ formatTokenId(identifier));
return;
}
PrivateKey privateKey = dKey.getPrivateKey();
byte[] password = createPassword(identifier.getBytes(), privateKey);
if (identifier.getSequenceNumber() > getDelegationTokenSeqNum()) {
setDelegationTokenSeqNum(identifier.getSequenceNumber());
}
if (currentTokens.get(identifier) == null) {
currentTokens.put(identifier, new TokenInfo(renewDate,
password, identifier.getTrackingId()));
} else {
throw new IOException("Same delegation token being added twice: "
+ formatTokenId(identifier));
}
}
/**
* Should be called before this object is used.
*
* @param keyPair
* @throws IOException
*/
public void startThreads(KeyPair keyPair) throws IOException {
Preconditions.checkState(!running);
public synchronized void start(KeyPair keyPair) throws IOException {
Preconditions.checkState(!isRunning());
updateCurrentKey(keyPair);
removeExpiredKeys();
synchronized (this) {
running = true;
tokenRemoverThread = new Daemon(new ExpiredTokenRemover());
tokenRemoverThread.start();
}
}
public void stopThreads() {
if (LOG.isDebugEnabled()) {
LOG.debug("Stopping expired delegation token remover thread");
}
running = false;
if (tokenRemoverThread != null) {
synchronized (noInterruptsLock) {
tokenRemoverThread.interrupt();
}
try {
tokenRemoverThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(
"Unable to join on token removal thread", e);
}
}
setIsRunning(true);
}
/**
* Stops the OzoneSecretManager.
* Stops the OzoneDelegationTokenSecretManager.
*
* @throws IOException
*/
public void stop() throws IOException {
stopThreads();
if (this.store != null) {
this.store.close();
}
public synchronized void stop() throws IOException {
setIsRunning(false);
}
/**
* Update the current master key. This is called once by startThreads before
* tokenRemoverThread is created,
*/
private void updateCurrentKey(KeyPair keyPair) throws IOException {
LOG.info("Updating the current master key for generating tokens");
// TODO: fix me based on the certificate expire time to set the key
// expire time.
int newCurrentId = incrementCurrentKeyId();
OzoneSecretKey newKey = new OzoneSecretKey(newCurrentId, -1,
keyPair, maxKeyLength);
store.storeTokenMasterKey(newKey);
if (!allKeys.containsKey(newKey.getKeyId())) {
allKeys.put(newKey.getKeyId(), newKey);
public String getDefaultSignatureAlgorithm() {
return securityConfig.getSignatureAlgo();
}
synchronized (this) {
currentKey = newKey;
}
public long getTokenMaxLifetime() {
return tokenMaxLifetime;
}
/**
* Remove expired delegation tokens from cache and persisted store.
*/
private void removeExpiredToken() throws IOException {
long now = Time.monotonicNow();
synchronized (this) {
Iterator<Map.Entry<T,
TokenInfo>> i = currentTokens.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<T,
TokenInfo> entry = i.next();
long renewDate = entry.getValue().getRenewDate();
if (renewDate < now) {
i.remove();
store.removeToken(entry.getKey());
}
}
public long getTokenRenewInterval() {
return tokenRenewInterval;
}
public Text getService() {
return service;
}
/**
@ -547,52 +263,20 @@ public synchronized boolean isRunning() {
return running;
}
/**
* Returns expiry time of a token given its identifier.
*
* @param dtId DelegationTokenIdentifier of a token
* @return Expiry time of the token
* @throws IOException
*/
public long getTokenExpiryTime(T dtId)
throws IOException {
TokenInfo info = currentTokens.get(dtId);
if (info != null) {
return info.getRenewDate();
} else {
throw new IOException("No delegation token found for this identifier");
}
public void setIsRunning(boolean val) {
running = val;
}
private class ExpiredTokenRemover extends Thread {
private long lastTokenCacheCleanup;
public OzoneSecretKey getCurrentKey() {
return currentKey;
}
@Override
public void run() {
LOG.info("Starting expired delegation token remover thread, "
+ "tokenRemoverScanInterval=" + tokenRemoverScanInterval
/ (60 * 1000) + " min(s)");
try {
while (running) {
long now = Time.monotonicNow();
if (lastTokenCacheCleanup + tokenRemoverScanInterval
< now) {
removeExpiredToken();
lastTokenCacheCleanup = now;
}
try {
Thread.sleep(Math.min(5000,
tokenRemoverScanInterval)); // 5 seconds
} catch (InterruptedException ie) {
LOG.error("ExpiredTokenRemover received " + ie);
}
}
} catch (Throwable t) {
LOG.error("ExpiredTokenRemover thread received unexpected exception",
t);
Runtime.getRuntime().exit(-1);
}
public AtomicInteger getCurrentKeyId() {
return currentKeyId;
}
public AtomicInteger getTokenSequenceNumber() {
return tokenSequenceNumber;
}
}

View File

@ -0,0 +1,146 @@
/*
* 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.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos.BlockTokenSecretProto.AccessModeProto;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.test.LambdaTestUtils;
import org.apache.hadoop.util.Time;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.security.KeyPair;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
/**
* Test class for {@link OzoneBlockTokenSecretManager}.
*/
public class TestOzoneBlockTokenSecretManager {
private OzoneBlockTokenSecretManager secretManager;
private KeyPair keyPair;
private X509Certificate x509Certificate;
private long expiryTime;
private String omCertSerialId;
private static final String BASEDIR = GenericTestUtils
.getTempPath(TestOzoneBlockTokenSecretManager.class.getSimpleName());
@Before
public void setUp() throws Exception {
OzoneConfiguration conf = new OzoneConfiguration();
conf.set(HddsConfigKeys.OZONE_METADATA_DIRS, BASEDIR);
// Create Ozone Master key pair.
keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
expiryTime = Time.monotonicNow() + 60 * 60 * 24;
// Create Ozone Master certificate (SCM CA issued cert) and key store.
x509Certificate = KeyStoreTestUtil
.generateCertificate("CN=OzoneMaster", keyPair, 30, "SHA256withRSA");
omCertSerialId = x509Certificate.getSerialNumber().toString();
secretManager = new OzoneBlockTokenSecretManager(conf,
expiryTime, omCertSerialId);
secretManager.start(keyPair);
}
@After
public void tearDown() throws Exception {
secretManager = null;
}
@Test
public void testGenerateToken() throws Exception {
Token<OzoneBlockTokenIdentifier> token = secretManager.generateToken(
"101", EnumSet.allOf(AccessModeProto.class), 100);
OzoneBlockTokenIdentifier identifier =
OzoneBlockTokenIdentifier.readFieldsProtobuf(new DataInputStream(
new ByteArrayInputStream(token.getIdentifier())));
// Check basic details.
Assert.assertTrue(identifier.getBlockId().equals("101"));
Assert.assertTrue(identifier.getAccessModes().equals(EnumSet
.allOf(AccessModeProto.class)));
Assert.assertTrue(identifier.getOmCertSerialId().equals(omCertSerialId));
validateHash(token.getPassword(), token.getIdentifier());
}
@Test
public void testCreateIdentifierSuccess() throws Exception {
OzoneBlockTokenIdentifier btIdentifier = secretManager.createIdentifier(
"testUser", "101", EnumSet.allOf(AccessModeProto.class), 100);
// Check basic details.
Assert.assertTrue(btIdentifier.getOwnerId().equals("testUser"));
Assert.assertTrue(btIdentifier.getBlockId().equals("101"));
Assert.assertTrue(btIdentifier.getAccessModes().equals(EnumSet
.allOf(AccessModeProto.class)));
Assert.assertTrue(btIdentifier.getOmCertSerialId().equals(omCertSerialId));
byte[] hash = secretManager.createPassword(btIdentifier);
validateHash(hash, btIdentifier.getBytes());
}
/**
* Validate hash using public key of KeyPair.
* */
private void validateHash(byte[] hash, byte[] identifier) throws Exception {
Signature rsaSignature =
Signature.getInstance(secretManager.getDefaultSignatureAlgorithm());
rsaSignature.initVerify(keyPair.getPublic());
rsaSignature.update(identifier);
Assert.assertTrue(rsaSignature.verify(hash));
}
@Test
public void testCreateIdentifierFailure() throws Exception {
LambdaTestUtils.intercept(SecurityException.class,
"Ozone block token can't be created without owner and access mode "
+ "information.", () -> {
secretManager.createIdentifier();
});
}
@Test
public void testRenewToken() throws Exception {
LambdaTestUtils.intercept(UnsupportedOperationException.class,
"Renew token operation is not supported for ozone block" +
" tokens.", () -> {
secretManager.renewToken(null, null);
});
}
@Test
public void testCancelToken() throws Exception {
LambdaTestUtils.intercept(UnsupportedOperationException.class,
"Cancel token operation is not supported for ozone block" +
" tokens.", () -> {
secretManager.cancelToken(null, null);
});
}
}

View File

@ -18,10 +18,6 @@
package org.apache.hadoop.ozone.security;
import java.io.File;
import java.io.IOException;
import java.security.KeyPair;
import java.security.Signature;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
@ -38,19 +34,25 @@
import org.junit.Before;
import org.junit.Test;
/**
* Test class for {@link OzoneSecretManager}.
*/
public class TestOzoneSecretManager {
import java.io.File;
import java.io.IOException;
import java.security.KeyPair;
import java.security.Signature;
private OzoneSecretManager<OzoneTokenIdentifier> secretManager;
/**
* Test class for {@link OzoneDelegationTokenSecretManager}.
*/
public class TestOzoneDelegationTokenSecretManager {
private OzoneDelegationTokenSecretManager<OzoneTokenIdentifier>
secretManager;
private SecurityConfig securityConfig;
private KeyPair keyPair;
private long expiryTime;
private Text serviceRpcAdd;
private OzoneConfiguration conf;
private static final String BASEDIR = GenericTestUtils
.getTempPath(TestOzoneSecretManager.class.getSimpleName());
private static final String BASEDIR = GenericTestUtils.getTempPath(
TestOzoneDelegationTokenSecretManager.class.getSimpleName());
private final static Text TEST_USER = new Text("testUser");
private long tokenMaxLifetime = 1000 * 20;
private long tokenRemoverScanInterval = 1000 * 20;
@ -76,7 +78,7 @@ public void tearDown() throws IOException {
public void testCreateToken() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -94,7 +96,7 @@ public void testCreateToken() throws Exception {
public void testRenewTokenSuccess() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -110,7 +112,7 @@ public void testRenewTokenSuccess() throws Exception {
public void testRenewTokenFailure() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -127,7 +129,7 @@ public void testRenewTokenFailure() throws Exception {
public void testRenewTokenFailureMaxTime() throws Exception {
secretManager = createSecretManager(conf, 100,
100, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -145,7 +147,7 @@ public void testRenewTokenFailureMaxTime() throws Exception {
public void testRenewTokenFailureRenewalTime() throws Exception {
secretManager = createSecretManager(conf, 1000 * 10,
10, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -159,7 +161,7 @@ public void testRenewTokenFailureRenewalTime() throws Exception {
public void testCreateIdentifier() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
OzoneTokenIdentifier identifier = secretManager.createIdentifier();
// Check basic details.
Assert.assertTrue(identifier.getOwner().equals(new Text("")));
@ -171,7 +173,7 @@ public void testCreateIdentifier() throws Exception {
public void testCancelTokenSuccess() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -182,7 +184,7 @@ public void testCancelTokenSuccess() throws Exception {
public void testCancelTokenFailure() throws Exception {
secretManager = createSecretManager(conf, tokenMaxLifetime,
expiryTime, tokenRemoverScanInterval);
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
Token<OzoneTokenIdentifier> token = secretManager.createToken(TEST_USER,
TEST_USER,
TEST_USER);
@ -205,12 +207,12 @@ private void validateHash(byte[] hash, byte[] identifier) throws Exception {
}
/**
* Create instance of {@link OzoneSecretManager}.
* Create instance of {@link OzoneDelegationTokenSecretManager}.
*/
private OzoneSecretManager<OzoneTokenIdentifier> createSecretManager(
OzoneConfiguration config, long tokenMaxLife, long expiry, long
tokenRemoverScanTime) throws IOException {
return new OzoneSecretManager<>(config, tokenMaxLife,
private OzoneDelegationTokenSecretManager<OzoneTokenIdentifier>
createSecretManager(OzoneConfiguration config, long tokenMaxLife,
long expiry, long tokenRemoverScanTime) throws IOException {
return new OzoneDelegationTokenSecretManager<>(config, tokenMaxLife,
expiry, tokenRemoverScanTime, serviceRpcAdd);
}
}

View File

@ -47,9 +47,9 @@
import org.apache.hadoop.ipc.ProtobufRpcEngine;
import org.apache.hadoop.ipc.RPC;
import org.apache.hadoop.ozone.OzoneSecurityUtil;
import org.apache.hadoop.ozone.security.OzoneSecretManager;
import org.apache.hadoop.ozone.security.OzoneSecurityException;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
import org.apache.hadoop.ozone.security.OzoneSecretManager;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
import org.apache.hadoop.metrics2.util.MBeans;
@ -95,6 +95,7 @@
import org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.ozone.security.acl.RequestContext;
import org.apache.hadoop.ozone.security.OzoneDelegationTokenSecretManager;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
@ -111,6 +112,7 @@
import org.slf4j.LoggerFactory;
import javax.management.ObjectName;
import javax.ws.rs.HEAD;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
@ -128,7 +130,6 @@
import java.util.Timer;
import java.util.TimerTask;
import static org.apache.hadoop.ozone.security.OzoneSecurityException.ResultCodes.*;
import static org.apache.hadoop.hdds.HddsUtils.getScmAddressForBlockClients;
import static org.apache.hadoop.hdds.HddsUtils.getScmAddressForClients;
import static org.apache.hadoop.hdds.HddsUtils.isHddsEnabled;
@ -178,8 +179,8 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
+ StartupOption.HELP.getName() + " ]\n";
private static final String OM_DAEMON = "om";
private static boolean securityEnabled = false;
private static OzoneSecretManager secretManager;
// TO DO: For testing purpose only, remove before commiting
private static OzoneDelegationTokenSecretManager<OzoneTokenIdentifier>
secretManager;
private KeyPair keyPair;
private CertificateClient certClient;
private static boolean testSecureOmFlag = false;
@ -367,9 +368,8 @@ private File getMetricsStorageFile() {
}
private OzoneSecretManager createSecretManager(
OzoneConfiguration conf)
throws IOException {
private OzoneDelegationTokenSecretManager createSecretManager(
OzoneConfiguration conf) throws IOException {
long tokenRemoverScanInterval =
conf.getTimeDuration(OMConfigKeys.DELEGATION_REMOVER_SCAN_INTERVAL_KEY,
OMConfigKeys.DELEGATION_REMOVER_SCAN_INTERVAL_DEFAULT,
@ -383,9 +383,8 @@ private OzoneSecretManager createSecretManager(
OMConfigKeys.DELEGATION_TOKEN_RENEW_INTERVAL_DEFAULT,
TimeUnit.MILLISECONDS);
Text omRpcAddressTxt = new Text(OmUtils.getOmRpcAddress(configuration));
return new OzoneSecretManager(conf, tokenMaxLifetime, tokenRenewInterval,
tokenRemoverScanInterval, omRpcAddressTxt);
return new OzoneDelegationTokenSecretManager<>(conf, tokenMaxLifetime,
tokenRenewInterval, tokenRemoverScanInterval, omRpcAddressTxt);
}
private void stopSecretManager() throws IOException {
@ -400,7 +399,7 @@ private void startSecretManager() {
try {
readKeyPair();
LOG.info("Starting OM secret manager");
secretManager.startThreads(keyPair);
secretManager.start(keyPair);
} catch (IOException e) {
// Inability to start secret manager
// can't be recovered from.
@ -424,7 +423,8 @@ private void readKeyPair() throws OzoneSecurityException {
certClient.getPrivateKey(OM_DAEMON));
} catch (Exception e) {
throw new OzoneSecurityException("Error reading private file for "
+ "OzoneManager", e, OM_PUBLIC_PRIVATE_KEY_FILE_NOT_EXIST);
+ "OzoneManager", e, OzoneSecurityException
.ResultCodes.OM_PUBLIC_PRIVATE_KEY_FILE_NOT_EXIST);
}
}

View File

@ -0,0 +1,251 @@
/*
* 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.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.Time;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Test class for OzoneManagerDelegationToken.
*/
public class TestOzoneManagerBlockToken {
private static final Logger LOG = LoggerFactory
.getLogger(TestOzoneManagerBlockToken.class);
private static final String BASEDIR = GenericTestUtils
.getTempPath(TestOzoneManagerBlockToken.class.getSimpleName());
private static final String KEYSTORES_DIR =
new File(BASEDIR).getAbsolutePath();
private static long expiryTime;
private static KeyPair keyPair;
private static X509Certificate cert;
private static final long MAX_LEN = 1000;
@BeforeClass
public static void setUp() throws Exception {
File base = new File(BASEDIR);
FileUtil.fullyDelete(base);
base.mkdirs();
expiryTime = Time.monotonicNow() + 60 * 60 * 24;
// Create Ozone Master key pair.
keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
// Create Ozone Master certificate (SCM CA issued cert) and key store.
cert = KeyStoreTestUtil
.generateCertificate("CN=OzoneMaster", keyPair, 30, "SHA256withRSA");
}
@After
public void cleanUp() {
}
@Test
public void testSignToken() throws GeneralSecurityException, IOException {
String keystore = new File(KEYSTORES_DIR, "keystore.jks")
.getAbsolutePath();
String truststore = new File(KEYSTORES_DIR, "truststore.jks")
.getAbsolutePath();
String trustPassword = "trustPass";
String keyStorePassword = "keyStorePass";
String keyPassword = "keyPass";
KeyStoreTestUtil.createKeyStore(keystore, keyStorePassword, keyPassword,
"OzoneMaster", keyPair.getPrivate(), cert);
// Create trust store and put the certificate in the trust store
Map<String, X509Certificate> certs = Collections.singletonMap("server",
cert);
KeyStoreTestUtil.createTrustStore(truststore, trustPassword, certs);
// Sign the OzoneMaster Token with Ozone Master private key
PrivateKey privateKey = keyPair.getPrivate();
OzoneBlockTokenIdentifier tokenId = new OzoneBlockTokenIdentifier(
"testUser", "84940",
EnumSet.allOf(HddsProtos.BlockTokenSecretProto.AccessModeProto.class),
expiryTime, cert.getSerialNumber().toString(), MAX_LEN);
byte[] signedToken = signTokenAsymmetric(tokenId, privateKey);
// Verify a valid signed OzoneMaster Token with Ozone Master
// public key(certificate)
boolean isValidToken = verifyTokenAsymmetric(tokenId, signedToken, cert);
LOG.info("{} is {}", tokenId, isValidToken ? "valid." : "invalid.");
// Verify an invalid signed OzoneMaster Token with Ozone Master
// public key(certificate)
tokenId = new OzoneBlockTokenIdentifier("", "",
EnumSet.allOf(HddsProtos.BlockTokenSecretProto.AccessModeProto.class),
expiryTime, cert.getSerialNumber().toString(), MAX_LEN);
LOG.info("Unsigned token {} is {}", tokenId,
verifyTokenAsymmetric(tokenId, RandomUtils.nextBytes(128), cert));
}
public byte[] signTokenAsymmetric(OzoneBlockTokenIdentifier tokenId,
PrivateKey privateKey) throws NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
Signature rsaSignature = Signature.getInstance("SHA256withRSA");
rsaSignature.initSign(privateKey);
rsaSignature.update(tokenId.getBytes());
byte[] signature = rsaSignature.sign();
return signature;
}
public boolean verifyTokenAsymmetric(OzoneBlockTokenIdentifier tokenId,
byte[] signature, Certificate certificate) throws InvalidKeyException,
NoSuchAlgorithmException, SignatureException {
Signature rsaSignature = Signature.getInstance("SHA256withRSA");
rsaSignature.initVerify(certificate);
rsaSignature.update(tokenId.getBytes());
boolean isValid = rsaSignature.verify(signature);
return isValid;
}
private byte[] signTokenSymmetric(OzoneBlockTokenIdentifier identifier,
Mac mac, SecretKey key) {
try {
mac.init(key);
} catch (InvalidKeyException ike) {
throw new IllegalArgumentException("Invalid key to HMAC computation",
ike);
}
return mac.doFinal(identifier.getBytes());
}
OzoneBlockTokenIdentifier generateTestToken() {
return new OzoneBlockTokenIdentifier(RandomStringUtils.randomAlphabetic(6),
RandomStringUtils.randomAlphabetic(5),
EnumSet.allOf(HddsProtos.BlockTokenSecretProto.AccessModeProto.class),
expiryTime, cert.getSerialNumber().toString(), MAX_LEN);
}
@Test
public void testAsymmetricTokenPerf() throws NoSuchAlgorithmException,
CertificateEncodingException, NoSuchProviderException,
InvalidKeyException, SignatureException {
final int testTokenCount = 1000;
List<OzoneBlockTokenIdentifier> tokenIds = new ArrayList<>();
List<byte[]> tokenPasswordAsym = new ArrayList<>();
for (int i = 0; i < testTokenCount; i++) {
tokenIds.add(generateTestToken());
}
KeyPair kp = KeyStoreTestUtil.generateKeyPair("RSA");
// Create Ozone Master certificate (SCM CA issued cert) and key store
X509Certificate omCert;
omCert = KeyStoreTestUtil.generateCertificate("CN=OzoneMaster",
kp, 30, "SHA256withRSA");
long startTime = Time.monotonicNowNanos();
for (int i = 0; i < testTokenCount; i++) {
tokenPasswordAsym.add(
signTokenAsymmetric(tokenIds.get(i), kp.getPrivate()));
}
long duration = Time.monotonicNowNanos() - startTime;
LOG.info("Average token sign time with HmacSha256(RSA/1024 key) is {} ns",
duration / testTokenCount);
startTime = Time.monotonicNowNanos();
for (int i = 0; i < testTokenCount; i++) {
verifyTokenAsymmetric(tokenIds.get(i), tokenPasswordAsym.get(i), omCert);
}
duration = Time.monotonicNowNanos() - startTime;
LOG.info("Average token verify time with HmacSha256(RSA/1024 key) "
+ "is {} ns", duration / testTokenCount);
}
@Test
public void testSymmetricTokenPerf() {
String hmacSHA1 = "HmacSHA1";
String hmacSHA256 = "HmacSHA256";
testSymmetricTokenPerfHelper(hmacSHA1, 64);
testSymmetricTokenPerfHelper(hmacSHA256, 1024);
}
public void testSymmetricTokenPerfHelper(String hmacAlgorithm, int keyLen) {
final int testTokenCount = 1000;
List<OzoneBlockTokenIdentifier> tokenIds = new ArrayList<>();
List<byte[]> tokenPasswordSym = new ArrayList<>();
for (int i = 0; i < testTokenCount; i++) {
tokenIds.add(generateTestToken());
}
KeyGenerator keyGen;
try {
keyGen = KeyGenerator.getInstance(hmacAlgorithm);
keyGen.init(keyLen);
} catch (NoSuchAlgorithmException nsa) {
throw new IllegalArgumentException("Can't find " + hmacAlgorithm +
" algorithm.");
}
Mac mac;
try {
mac = Mac.getInstance(hmacAlgorithm);
} catch (NoSuchAlgorithmException nsa) {
throw new IllegalArgumentException("Can't find " + hmacAlgorithm +
" algorithm.");
}
SecretKey secretKey = keyGen.generateKey();
long startTime = Time.monotonicNowNanos();
for (int i = 0; i < testTokenCount; i++) {
tokenPasswordSym.add(
signTokenSymmetric(tokenIds.get(i), mac, secretKey));
}
long duration = Time.monotonicNowNanos() - startTime;
LOG.info("Average token sign time with {}({} symmetric key) is {} ns",
hmacAlgorithm, keyLen, duration / testTokenCount);
}
}