[java-identity-provider] branch master updated: IDP-1275 - Deferred decryption of private key

Scott Cantor cantor.2 at osu.edu
Tue Jul 31 17:06:43 EDT 2018


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=a6ce06113d03b3e76958d6bf5fbdd57c76fa496f

The following commit(s) were added to refs/heads/master by this push:
       new  a6ce061   IDP-1275 - Deferred decryption of private key
a6ce061 is described below

commit a6ce06113d03b3e76958d6bf5fbdd57c76fa496f
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jul 31 17:06:39 2018 -0400

    IDP-1275 - Deferred decryption of private key
    
    https://issues.shibboleth.net/jira/browse/IDP-1275
    
    Lightly tested webflow with some defaults/examples.
---
 .../idp/admin/impl/UnlockDataSealers.java          | 150 ++++++++++++++++++
 .../idp/admin/impl/UnlockPrivateKeys.java          | 176 +++++++++++++++++++++
 .../main/resources/conf/admin/general-admin.xml    |   9 ++
 .../resources/system/conf/general-admin-system.xml |   1 +
 .../main/resources/system/conf/global-system.xml   |   5 +-
 .../main/resources/system/conf/webflow-config.xml  |   1 +
 .../system/flows/admin/unlock-keys-beans.xml       |  45 ++++++
 .../system/flows/admin/unlock-keys-flow.xml        |  65 ++++++++
 .../resources/system/messages/messages.properties  |  11 ++
 .../src/main/resources/views/admin/unlock-keys.vm  |  96 +++++++++++
 10 files changed, 557 insertions(+), 2 deletions(-)

diff --git a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockDataSealers.java b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockDataSealers.java
new file mode 100644
index 0000000..cc73106
--- /dev/null
+++ b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockDataSealers.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.idp.admin.impl;
+
+import java.security.KeyException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.security.BasicKeystoreKeyStrategy;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+
+/**
+ * Action that sets keystore and key passwords for one or more DataSealer KeyStrategy
+ * objects based on query parameters.
+ * 
+ * <p>The only type supported is the basic strategy type provided with the software.</p>
+ * 
+ * <p>An error event will be signaled after the first unsuccessful unlock operation.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * 
+ * @since 3.4.0
+ */
+public class UnlockDataSealers extends AbstractProfileAction {
+    
+    /** Name of keystore password parameter. */
+    @Nonnull @NotEmpty public static final String KEYSTORE_PASSWORD_PARAM_NAME = "keystorePassword";
+
+    /** Name of key password parameter. */
+    @Nonnull @NotEmpty public static final String KEY_PASSWORD_PARAM_NAME = "keyPassword";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(UnlockDataSealers.class);
+    
+    /** Key source(s) to operate on. */
+    @Nonnull @NonnullElements private Collection<BasicKeystoreKeyStrategy> keyStrategies;
+
+    /** Constructor. */
+    public UnlockDataSealers() {
+        keyStrategies = Collections.emptyList();
+    }
+    
+    /**
+     * Set the {@link BasicKeystoreKeyStrategy} objects to inject passwords into.
+     * 
+     * @param strategies objects to unlock
+     */
+    public void setKeyStrategies(@Nullable @NonnullElements final Collection<BasicKeystoreKeyStrategy> strategies) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        if (strategies != null && !strategies.isEmpty()) {
+            keyStrategies = new ArrayList<>(Collections2.filter(strategies, Predicates.notNull()));
+        } else {
+            keyStrategies = Collections.emptyList();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext) || keyStrategies.isEmpty()) {
+            return false;
+        } else if (getHttpServletRequest() == null) {
+            log.warn("{} No HttpServletRequest available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(final ProfileRequestContext profileRequestContext) {
+
+        final HttpServletRequest request = getHttpServletRequest();
+        
+        final String[] keystorePasswords = request.getParameterValues(KEYSTORE_PASSWORD_PARAM_NAME);
+        final String[] keyPasswords = request.getParameterValues(KEY_PASSWORD_PARAM_NAME);
+        
+        if (keystorePasswords == null || keyPasswords == null || keystorePasswords.length != keyStrategies.size()
+                || keyPasswords.length != keyStrategies.size()) {
+            log.warn("{} Password parameter count does not match size of configured KeyStrategy inputs",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        int i = 0;
+        final Iterator<BasicKeystoreKeyStrategy> iter = keyStrategies.iterator();
+        while (iter.hasNext()) {
+            final BasicKeystoreKeyStrategy ks = iter.next();
+            
+            if (keystorePasswords[i] == null || keyPasswords[i] == null) {
+                log.warn("{} Empty password supplied at index {}", getLogPrefix(), i);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            
+            ks.setKeystorePassword(keystorePasswords[i]);
+            ks.setKeyPassword(keyPasswords[i]);
+            
+            try {
+                ks.getDefaultKey();
+            } catch (final KeyException e) {
+                log.warn("{} Failed to unlock key strategy in collection with index {}", getLogPrefix(), i);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            
+            i++;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockPrivateKeys.java b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockPrivateKeys.java
new file mode 100644
index 0000000..0b4758d
--- /dev/null
+++ b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/UnlockPrivateKeys.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.idp.admin.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+
+import org.cryptacular.EncodingException;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.MutableCredential;
+import org.opensaml.security.crypto.KeySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+
+/**
+ * Action that creates private key objects and injects them into existing
+ * {@link MutableCredential} objects.
+ * 
+ * <p>An error event will be signaled after the first unsuccessful unlock operation.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * 
+ * @since 3.4.0
+ */
+public class UnlockPrivateKeys extends AbstractProfileAction {
+    
+    /** Name of private key password parameter. */
+    @Nonnull @NotEmpty public static final String KEY_PASSWORD_PARAM_NAME = "privateKeyPassword";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(UnlockPrivateKeys.class);
+    
+    /** Credentials to operate on. */
+    @Nonnull @NonnullElements private Collection<MutableCredential> credentials;
+
+    /** Keys to load. */
+    @Nonnull @NonnullElements private Collection<Resource> keyResources;
+
+    /** Constructor. */
+    public UnlockPrivateKeys() {
+        credentials = Collections.emptyList();
+        keyResources = Collections.emptyList();
+    }
+    
+    /**
+     * Set the credentials to load keys into.
+     * 
+     * @param creds credentials to load keys into
+     */
+    public void setCredentials(@Nullable @NonnullElements final Collection<MutableCredential> creds) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        if (creds != null && !creds.isEmpty()) {
+            credentials = new ArrayList<>(Collections2.filter(creds, Predicates.notNull()));
+        } else {
+            credentials = Collections.emptyList();
+        }
+    }
+
+    /**
+     * Set the key resources to load.
+     * 
+     * @param keys key resources to load
+     */
+    public void setKeyResources(@Nullable @NonnullElements final Collection<Resource> keys) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        if (keys != null && !keys.isEmpty()) {
+            keyResources = new ArrayList<>(Collections2.filter(keys, Predicates.notNull()));
+        } else {
+            keyResources = Collections.emptyList();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (keyResources.size() != credentials.size()) {
+            throw new ComponentInitializationException("Size of credential and key resource collections don't match.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext) || credentials.isEmpty() || keyResources.isEmpty()) {
+            return false;
+        } else if (getHttpServletRequest() == null) {
+            log.warn("{} No HttpServletRequest available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(final ProfileRequestContext profileRequestContext) {
+
+        final String[] keyPasswords = getHttpServletRequest().getParameterValues(KEY_PASSWORD_PARAM_NAME);
+        
+        if (keyPasswords == null || keyPasswords.length != credentials.size()) {
+            log.warn("{} Password parameter count does not match number of configured credentials", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        int i = 0;
+        Iterator<MutableCredential> c_iter = credentials.iterator();
+        Iterator<Resource> k_iter = keyResources.iterator();
+        while (c_iter.hasNext() && k_iter.hasNext()) {
+            final MutableCredential cred = c_iter.next();
+            final Resource resource = k_iter.next();
+            
+            if (keyPasswords[i] == null) {
+                log.warn("{} Empty password supplied at index {}", getLogPrefix(), i);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+
+            log.info("{} Attempting unlock of private key in {}", getLogPrefix(), resource.getDescription());
+            
+            try (final InputStream is = resource.getInputStream()) {
+                cred.setPrivateKey(KeySupport.decodePrivateKey(is, keyPasswords[i].toCharArray()));
+                log.info("{} Unlocked and injected private key in {}", getLogPrefix(), resource.getDescription());
+            } catch (final KeyException | IOException | EncodingException e) {
+                log.warn("{} Failed to unlock private key with index {}", getLogPrefix(), i, e);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            
+            i++;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/conf/admin/general-admin.xml b/idp-conf/src/main/resources/conf/admin/general-admin.xml
index 5301fe7..ca1d877 100644
--- a/idp-conf/src/main/resources/conf/admin/general-admin.xml
+++ b/idp-conf/src/main/resources/conf/admin/general-admin.xml
@@ -59,6 +59,15 @@
             c:id="http://shibboleth.net/ns/profiles/metrics"
             p:loggingId="Metrics"
             p:policyNameLookupStrategy-ref="shibboleth.metrics.AccessPolicyStrategy" />
+
+        <!-- Attended Startup Unlock -->
+        <!--
+        <bean parent="shibboleth.AdminFlow"
+            c:id="http://shibboleth.net/ns/profiles/unlock-keys"
+            p:loggingId="UnlockKeys"
+            p:authenticated="true"
+            p:policyName="AccessByAdminUser" />
+        -->
     
     </util:list>
 
diff --git a/idp-conf/src/main/resources/system/conf/general-admin-system.xml b/idp-conf/src/main/resources/system/conf/general-admin-system.xml
index 32d1066..1404dc6 100644
--- a/idp-conf/src/main/resources/system/conf/general-admin-system.xml
+++ b/idp-conf/src/main/resources/system/conf/general-admin-system.xml
@@ -14,6 +14,7 @@
 
     <import resource="../../conf/admin/general-admin.xml" />
     <import resource="../../conf/admin/metrics.xml" />
+    <import resource="conditional:${idp.home}/conf/admin/unlock-keys.xml" />
 
     <!-- A parent bean to default some of the flow boilerplate. -->
 
diff --git a/idp-conf/src/main/resources/system/conf/global-system.xml b/idp-conf/src/main/resources/system/conf/global-system.xml
index 4e6102f..bf32695 100644
--- a/idp-conf/src/main/resources/system/conf/global-system.xml
+++ b/idp-conf/src/main/resources/system/conf/global-system.xml
@@ -209,14 +209,15 @@
         p:keystoreType="%{idp.sealer.storeType:JCEKS}"
         p:keystoreResource="%{idp.sealer.storeResource:}"
         p:keyVersionResource="%{idp.sealer.versionResource:}"
-        p:keystorePassword="%{idp.sealer.storePassword:}"
         p:keyAlias="%{idp.sealer.aliasBase:secret}"
+        p:keystorePassword="%{idp.sealer.storePassword:}"
         p:keyPassword="%{idp.sealer.keyPassword:}"
         p:updateInterval="%{idp.sealer.updateInterval:PT15M}" />
 
     <bean id="shibboleth.DataSealer" lazy-init="true"
         class="net.shibboleth.utilities.java.support.security.DataSealer"
-        p:keyStrategy-ref="shibboleth.DataSealerKeyStrategy" />
+        p:keyStrategy-ref="shibboleth.DataSealerKeyStrategy"
+        p:lockedAtStartup="#{ '%{idp.sealer.storePassword:}'.isEmpty() or '%{idp.sealer.keyPassword:}'.isEmpty() }" />
 
     <!-- This is a convenience for compatibility with the examples for configuring this in V2. -->
     <alias name="shibboleth.DataSealer" alias="shibboleth.TransientIDDataSealer"/>
diff --git a/idp-conf/src/main/resources/system/conf/webflow-config.xml b/idp-conf/src/main/resources/system/conf/webflow-config.xml
index b8e3cb0..6cb8c1d 100644
--- a/idp-conf/src/main/resources/system/conf/webflow-config.xml
+++ b/idp-conf/src/main/resources/system/conf/webflow-config.xml
@@ -30,6 +30,7 @@
                 <entry key="admin/mdquery" value="../system/flows/admin/mdquery-flow.xml" />
                 <entry key="admin/metrics" value="../system/flows/admin/metrics-flow.xml" />
                 <entry key="admin/storage" value="../system/flows/admin/storage-flow.xml" />
+                <entry key="admin/unlock-keys" value="../system/flows/admin/unlock-keys-flow.xml" />
                 
                 <!-- Proprietary logout flow. -->
                 <entry key="Logout" value="../system/flows/logout/logout-flow.xml" />
diff --git a/idp-conf/src/main/resources/system/flows/admin/unlock-keys-beans.xml b/idp-conf/src/main/resources/system/flows/admin/unlock-keys-beans.xml
new file mode 100644
index 0000000..5fa1c57
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/admin/unlock-keys-beans.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans
+    xmlns="http://www.springframework.org/schema/beans"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize"
+    default-destroy-method="destroy">
+
+    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <import resource="admin-abstract-beans.xml" />
+
+    <!-- Profile ID for flow. -->    
+    <bean id="shibboleth.AdminProfileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/unlock-keys" />
+    
+    <!-- Default operation/resource suppliers for access checks. -->
+
+    <bean id="shibboleth.AdminOperationLookupStrategy" class="com.google.common.base.Functions"
+        factory-method="constant" c:_0="unlock" />
+        
+    <bean id="shibboleth.AdminResourceLookupStrategy" class="com.google.common.base.Functions"
+        factory-method="constant" c:_0="keys" />
+    
+    <!--  Work beans. -->
+    
+    <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.PostResponseAuditExtractors') ?: getObject('shibboleth.DefaultPostResponseAuditExtractors')}" />
+    
+    <bean id="UnlockDataSealers" class="net.shibboleth.idp.admin.impl.UnlockDataSealers" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:keyStrategies="#{getObject('shibboleth.unlock-keys.KeyStrategies')}" />
+
+    <bean id="UnlockPrivateKeys" class="net.shibboleth.idp.admin.impl.UnlockPrivateKeys" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:credentials="#{getObject('shibboleth.unlock-keys.Credentials')}"
+        p:keyResources="#{getObject('shibboleth.unlock-keys.PrivateKeys')}" />
+        
+</beans>
diff --git a/idp-conf/src/main/resources/system/flows/admin/unlock-keys-flow.xml b/idp-conf/src/main/resources/system/flows/admin/unlock-keys-flow.xml
new file mode 100644
index 0000000..7a25554
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/admin/unlock-keys-flow.xml
@@ -0,0 +1,65 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="admin.abstract">
+
+    <!-- Start action. -->
+        
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to determine if authentication is required. -->
+        <transition on="proceed" to="DoAdminPreamble" />
+    </action-state>
+
+    <!-- Resume actual flow processing. -->
+
+    <action-state id="DoProfileWork">
+        <evaluate expression="CheckAccess" />
+        <evaluate expression="PostResponsePopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PromptForPasswords" />
+    </action-state>
+    
+    <view-state id="PromptForPasswords" view="admin/unlock-keys">
+        <on-render>
+            <evaluate expression="environment" result="viewScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+        </on-render>
+
+        <transition on="proceed" to="UnlockKeys" />
+        <transition on="cancel" to="end" />
+    </view-state>
+
+    <action-state id="UnlockKeys">
+        <evaluate expression="UnlockDataSealers" />
+        <evaluate expression="UnlockPrivateKeys" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="end" />
+        <transition on="InvalidMessage" to="PromptForPasswords" />
+    </action-state>
+    
+    <!-- Terminus -->
+
+    <end-state id="end" view="admin/unlock-keys">
+        <on-entry>
+            <evaluate expression="WriteAuditLog" />
+            <evaluate expression="environment" result="requestScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="requestScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="requestScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="requestScope.response" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="requestScope.custom" />
+        </on-entry>
+    </end-state>
+    
+    <bean-import resource="unlock-keys-beans.xml" />
+
+</flow>
diff --git a/idp-conf/src/main/resources/system/messages/messages.properties b/idp-conf/src/main/resources/system/messages/messages.properties
index c0a2f9c..29eae36 100644
--- a/idp-conf/src/main/resources/system/messages/messages.properties
+++ b/idp-conf/src/main/resources/system/messages/messages.properties
@@ -177,6 +177,17 @@ idp.impersonate.header = Account Impersonation
 idp.impersonate.login-as = Login as
 idp.impersonate.proceed = Proceed
 
+# General messages related to key-unlocking admin flow
+
+idp.unlock-keys.title = Attended Restart Key Unlock
+idp.unlock-keys.keystorePassword = DataSealer Keystore Password
+idp.unlock-keys.keyPassword = DataSealer Key Password
+idp.unlock-keys.privateKeyPassword = Private Key Password
+idp.unlock-keys.unlock = Unlock
+idp.unlock-keys.cancel = Cancel
+idp.unlock-keys.complete = The system is unlocked and ready for use.
+idp.unlock-keys.error = Unlock failed; check log for specific message.
+
 # Triples consisting of a TOU key, and a title and text for each set of terms.
 # The default implementation uses the SP name as the key, but this can be overriden.
 
diff --git a/idp-conf/src/main/resources/views/admin/unlock-keys.vm b/idp-conf/src/main/resources/views/admin/unlock-keys.vm
new file mode 100644
index 0000000..e91123e
--- /dev/null
+++ b/idp-conf/src/main/resources/views/admin/unlock-keys.vm
@@ -0,0 +1,96 @@
+##
+## Velocity Template for Attended Startup Unlock Utility
+##
+## Velocity context will contain the following properties:
+## flowRequestContext - the Spring Web Flow RequestContext
+## request - HttpServletRequest
+## response - HttpServletResponse
+## profileRequestContext
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($title = $springMacroRequestContext.getMessage("idp.title", "Web Login Service"))
+#set ($titleSuffix = $springMacroRequestContext.getMessage("idp.unlock-keys.title", "Attended Restart Key Unlock"))
+#set ($eventId = $profileRequestContext.getSubcontext("org.opensaml.profile.context.EventContext").getEvent())
+#set ($state = $flowRequestContext.getCurrentState().getId())
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width,initial-scale=1.0">
+        <title>$title - $titleSuffix</title>
+        <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/main.css">
+    </head>
+    
+    <body>
+    <div class="wrapper">
+        <div class="container">
+            <header>
+                <img src="$request.getContextPath()#springMessage("idp.logo")" alt="#springMessageText("idp.logo.alt-text", "logo")">
+                <h3>$title - $titleSuffix</h3>
+            </header>
+        
+            <div class="content">
+            #if ($state == "end")
+                <strong>#springMessageText("idp.unlock-keys.complete", "The system is unlocked and ready for use.")</strong>
+                <p><a hef="$request.getContextPath()/profile/SAML2/SSO/Unsolicited?providerId=https://sp.example.org/shibboleth">Validation Link</a></p>
+            #else
+                #if ($eventId == "InvalidMessage")
+                    <p class="form-element form-error">
+                    #springMessageText("idp.unlock-keys.error", "Unlock failed; check log for specific message.")
+                    </p>
+                    <br/><br/>
+                #end
+            
+                <form action="$flowExecutionUrl" method="post">
+                
+                  <!--
+                  If you have multiple key strategies defined, you'll need multiple pairs of form fields for
+                  the passwords, labeled in the order they're fed into the shibboleth.unlock-keys.KeyStrategies
+                  bean. If you have none, remove the fields.
+                  -->
+
+                  <div class="form-element-wrapper">
+                    <label for="password">#springMessageText("idp.unlock-keys.keystorePassword", "DataSealer Keystore Password")</label>
+                    <input class="form-element form-field" id="password" name="keystorePassword" type="password">
+                  </div>
+
+                  <div class="form-element-wrapper">
+                    <label for="password">#springMessageText("idp.unlock-keys.keyPassword", "DataSealer Key Password")</label>
+                    <input class="form-element form-field" id="password" name="keyPassword" type="password">
+                  </div>
+
+                  <!--
+                  If you have multiple private keys defined, you'll need a form field for each passwords, labeled in the order
+                  they're fed into the shibboleth.unlock-keys.Credentials/PrivateKeys beans. If you have none, remove the fields.
+                  -->
+
+                  <div class="form-element-wrapper">
+                    <label for="password">#springMessageText("idp.unlock-keys.privateKeyPassword", "Private Key Password")</label>
+                    <input class="form-element form-field" id="password" name="privateKeyPassword" type="password">
+                  </div>
+                  
+                  <div class="form-element-wrapper">
+                    <button class="form-element form-button" type="submit" name="_eventId_proceed"
+                        >#springMessageText("idp.unlock-keys.unlock", "Unlock")</button>
+                  </div>
+
+                  <div class="form-element-wrapper">
+                    <button class="form-element form-button" type="submit" name="_eventId_cancel"
+                        >#springMessageText("idp.unlock-keys.cancel", "Cancel")</button>
+                  </div>
+                  
+                </form>
+            #end
+            </div>
+        </div>
+
+        <footer>
+            <div class="container container-footer">
+                <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+            </div>
+        </footer>
+        
+    </div>
+    </body>
+</html>
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list