diff --git a/lib/SAML2/Binding.php b/lib/SAML2/Binding.php
index a6b672b3cdfb37f9327d63e37c190a334c44999d..cc5f1ccb9083a329330be97bb5ae94e28f34895c 100644
--- a/lib/SAML2/Binding.php
+++ b/lib/SAML2/Binding.php
@@ -34,6 +34,8 @@ abstract class SAML2_Binding {
 			return new SAML2_HTTPRedirect();
 		case SAML2_Const::BINDING_HTTP_ARTIFACT:
 			return new SAML2_HTTPArtifact();
+		case SAML2_Const::BINDING_HOK_SSO:
+			return new SAML2_HTTPPost();
 		default:
 			throw new Exception('Unsupported binding: ' . var_export($urn, TRUE));
 		}
diff --git a/lib/SAML2/Const.php b/lib/SAML2/Const.php
index d44a28e0383488b811329770812c425186ba01dc..c5361867becb0b98de4770ef115e920c1277841c 100644
--- a/lib/SAML2/Const.php
+++ b/lib/SAML2/Const.php
@@ -39,12 +39,22 @@ class SAML2_Const {
 	 */
 	const BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP';
 
+	/**
+	 * The URN for the Holder-of-Key Web Browser SSO Profile binding
+	 */
+	const BINDING_HOK_SSO = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser';
+
 
 	/**
 	 * Bearer subject confirmation method.
 	 */
 	const CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer';
 
+	/**
+	* Holder-of-Key subject confirmation method.
+	*/
+	const CM_HOK = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key';
+
 
 	/**
 	 * The URN for the unspecified attribute NameFormat.
@@ -103,6 +113,10 @@ class SAML2_Const {
 	 */
 	const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
 
+	/**
+	 * The namespace for the SAML 2 HoK Web Browser SSO Profile.
+	 */
+	const NS_HOK = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser';
 
 	/**
 	 * Top-level status code indicating successful processing of the request.
diff --git a/lib/SAML2/XML/saml/SubjectConfirmationData.php b/lib/SAML2/XML/saml/SubjectConfirmationData.php
index 74fac0a2c2fcfebf60706a51310b9b07dd77b121..1c28c65c8df37559c5977d1a8893763eb64efd6f 100644
--- a/lib/SAML2/XML/saml/SubjectConfirmationData.php
+++ b/lib/SAML2/XML/saml/SubjectConfirmationData.php
@@ -49,7 +49,18 @@ class SAML2_XML_saml_SubjectConfirmationData {
 
 
 	/**
-	 * Initialize (and parse? a SubjectConfirmationData element.
+	 * The various key information elements.
+	 *
+	 * Array with various elements describing this key.
+	 * Unknown elements will be represented by SAML2_XML_Chunk.
+	 *
+	 * @var array
+	 */
+	public $info = array();
+
+
+	/**
+	 * Initialize (and parse) a SubjectConfirmationData element.
 	 *
 	 * @param DOMElement|NULL $xml  The XML element we should load.
 	 */
@@ -74,6 +85,23 @@ class SAML2_XML_saml_SubjectConfirmationData {
 		if ($xml->hasAttribute('Address')) {
 			$this->Address = $xml->getAttribute('Address');
 		}
+		for ($n = $xml->firstChild; $n !== NULL; $n = $n->nextSibling) {
+			if (!($n instanceof DOMElement)) {
+				continue;
+			}
+			if ($n->namespaceURI !== XMLSecurityDSig::XMLDSIGNS) {
+				$this->info[] = new SAML2_XML_Chunk($n);
+				continue;
+			}
+			switch ($n->localName) {
+			case 'KeyInfo':
+				$this->info[] = new SAML2_XML_ds_KeyInfo($n);
+				break;
+			default:
+				$this->info[] = new SAML2_XML_Chunk($n);
+				break;
+			}
+		}
 	}
 
 
@@ -108,6 +136,9 @@ class SAML2_XML_saml_SubjectConfirmationData {
 		if (isset($this->Address)) {
 			$e->setAttribute('Address', $this->Address);
 		}
+		foreach ($this->info as $n) {
+			$n->toXML($e);
+		}
 
 		return $e;
 	}
diff --git a/lib/SimpleSAML/Metadata/SAMLBuilder.php b/lib/SimpleSAML/Metadata/SAMLBuilder.php
index c15cbca1a86288559c5c4e6964736ba8cb8eb4c8..3373d0f64987df7419d88c73ae0a84efd071dda8 100644
--- a/lib/SimpleSAML/Metadata/SAMLBuilder.php
+++ b/lib/SimpleSAML/Metadata/SAMLBuilder.php
@@ -190,6 +190,9 @@ class SimpleSAML_Metadata_SAMLBuilder {
 			if (isset($ep['ResponseLocation'])) {
 				$t->ResponseLocation = $ep['ResponseLocation'];
 			}
+			if (isset($ep['hoksso:ProtocolBinding'])) {
+			    $t->setAttributeNS(SAML2_Const::NS_HOK, 'hoksso:ProtocolBinding', SAML2_Const::BINDING_HTTP_REDIRECT);
+			}
 
 			if ($indexed) {
 				if (!isset($ep['index'])) {
diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php
index 17d01b3ca1792072ed088c6c01df91d797436f53..15434a0362197ec6a14717c4bf47e358f43fbbd0 100644
--- a/modules/saml/lib/IdP/SAML2.php
+++ b/modules/saml/lib/IdP/SAML2.php
@@ -226,6 +226,9 @@ class sspmod_saml_IdP_SAML2 {
 		if ($idpMetadata->getBoolean('saml20.sendartifact', FALSE)) {
 			$supportedBindings[] = SAML2_Const::BINDING_HTTP_ARTIFACT;
 		}
+		if ($idpMetadata->getBoolean('saml20.hok.assertion', FALSE)) {
+			$supportedBindings[] = SAML2_Const::BINDING_HOK_SSO;
+		}
 
 		if (isset($_REQUEST['spentityid'])) {
 			/* IdP initiated authentication. */
@@ -763,11 +766,47 @@ class sspmod_saml_IdP_SAML2 {
 		$a->setSessionIndex(SimpleSAML_Utilities::generateID());
 
 		$sc = new SAML2_XML_saml_SubjectConfirmation();
-		$sc->Method = SAML2_Const::CM_BEARER;
 		$sc->SubjectConfirmationData = new SAML2_XML_saml_SubjectConfirmationData();
 		$sc->SubjectConfirmationData->NotOnOrAfter = time() + $assertionLifetime;
 		$sc->SubjectConfirmationData->Recipient = $state['saml:ConsumerURL'];
 		$sc->SubjectConfirmationData->InResponseTo = $state['saml:RequestId'];
+
+		/* ProtcolBinding of SP's <AuthnRequest> overwrites IdP hosted metadata configuration. */
+		$hokAssertion = NULL;
+		if ($state['saml:Binding'] === SAML2_Const::BINDING_HOK_SSO) {
+		    $hokAssertion = TRUE;
+		}
+		if ($hokAssertion === NULL) {
+			$hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', FALSE);
+		}
+
+		if ($hokAssertion) {
+			/* Holder-of-Key */
+			$sc->Method = SAML2_Const::CM_HOK;
+			if (SimpleSAML_Utilities::isHTTPS()) {
+				if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) {
+					/* Extract certificate data (if this is a certificate). */
+					$clientCert = $_SERVER['SSL_CLIENT_CERT'];
+					$pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m';
+					if (preg_match($pattern, $clientCert, $matches)) {
+						/* We have a client certificate from the browser which we add to the HoK assertion. */
+						$x509Certificate = new SAML2_XML_ds_X509Certificate();
+						$x509Certificate->certificate = str_replace(array("\r", "\n", " "), '', $matches[1]);
+
+						$x509Data = new SAML2_XML_ds_X509Data();
+						$x509Data->data[] = $x509Certificate;
+
+						$keyInfo = new SAML2_XML_ds_KeyInfo();
+						$keyInfo->info[] = $x509Data;
+
+						$sc->SubjectConfirmationData->info[] = $keyInfo;
+					} else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No valid client certificate provided during TLS handshake with IdP');
+				} else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No client certificate provided during TLS handshake with IdP');
+			} else throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO');
+		} else {
+			/* Bearer */
+			$sc->Method = SAML2_Const::CM_BEARER;
+		}
 		$a->setSubjectConfirmation(array($sc));
 
 		/* Add attributes. */
diff --git a/modules/saml/lib/Message.php b/modules/saml/lib/Message.php
index 65ebff37ff76feb60cfc441c14870cc9f1688c0c..105c54cd3efb04d7a457e0ad641c99117b5bb191 100644
--- a/modules/saml/lib/Message.php
+++ b/modules/saml/lib/Message.php
@@ -373,17 +373,12 @@ class sspmod_saml_Message {
 			));
 		}
 
-		$dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HTTP_REDIRECT));
-		$dst = $dst['Location'];
-
-		$ar->setIssuer($spMetadata->getString('entityid'));
-		$ar->setDestination($dst);
-
 		$ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', FALSE));
 		$ar->setIsPassive($spMetadata->getBoolean('IsPassive', FALSE));
 
 		$protbind = $spMetadata->getValueValidate('ProtocolBinding', array(
 				SAML2_Const::BINDING_HTTP_POST,
+				SAML2_Const::BINDING_HOK_SSO,
 				SAML2_Const::BINDING_HTTP_ARTIFACT,
 				SAML2_Const::BINDING_HTTP_REDIRECT,
 			), SAML2_Const::BINDING_HTTP_POST);
@@ -391,6 +386,17 @@ class sspmod_saml_Message {
 		/* Shoaib - setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST */
 		$ar->setProtocolBinding($protbind);
 
+		/* Select appropriate SSO endpoint */
+		if ($protbind === SAML2_Const::BINDING_HOK_SSO) {
+		    $dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HOK_SSO));
+		} else {
+		    $dst = $idpMetadata->getDefaultEndpoint('SingleSignOnService', array(SAML2_Const::BINDING_HTTP_REDIRECT));
+		}
+		$dst = $dst['Location'];
+
+		$ar->setIssuer($spMetadata->getString('entityid'));
+		$ar->setDestination($dst);
+
 		if ($spMetadata->hasValue('AuthnContextClassRef')) {
 			$accr = $spMetadata->getArrayizeString('AuthnContextClassRef');
 			$ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr));
@@ -559,11 +565,84 @@ class sspmod_saml_Message {
 		$found = FALSE;
 		$lastError = 'No SubjectConfirmation element in Subject.';
 		foreach ($assertion->getSubjectConfirmation() as $sc) {
-			if ($sc->Method !== SAML2_Const::CM_BEARER) {
+			if ($sc->Method !== SAML2_Const::CM_BEARER && $sc->Method !== SAML2_Const::CM_HOK) {
 				$lastError = 'Invalid Method on SubjectConfirmation: ' . var_export($sc->Method, TRUE);
 				continue;
 			}
+
+			/* Is SSO with HoK enabled? IdP remote metadata overwrites SP metadata configuration. */
+			$hok = $idpMetadata->getBoolean('saml20.hok.assertion', NULL);
+			if ($hok === NULL) {
+			    $protocolBinding = $spMetadata->getString('ProtocolBinding', SAML2_Const::BINDING_HTTP_POST);
+			    if ($protocolBinding === SAML2_Const::BINDING_HOK_SSO) {
+				$hok = TRUE;
+			    } else {
+				$hok = FALSE;
+			    }
+			}
+			if ($sc->Method === SAML2_Const::CM_BEARER && $hok) {
+				$lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed';
+				continue;
+			}
+
 			$scd = $sc->SubjectConfirmationData;
+			if ($sc->Method === SAML2_Const::CM_HOK) {
+				/* Check HoK Assertion */
+				if (SimpleSAML_Utilities::isHTTPS() === FALSE) {
+				    $lastError = 'No HTTPS connection, but required for Holder-of-Key SSO';
+				    continue;
+				}
+				if (isset($_SERVER['SSL_CLIENT_CERT']) && empty($_SERVER['SSL_CLIENT_CERT'])) {
+				    $lastError = 'No client certificate provided during TLS Handshake with SP';
+				    continue;
+				}
+				/* Extract certificate data (if this is a certificate). */
+				$clientCert = $_SERVER['SSL_CLIENT_CERT'];
+				$pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m';
+				if (preg_match($pattern, $clientCert, $matches) === FALSE) {
+				    $lastError = 'No valid client certificate provided during TLS Handshake with SP';
+				    continue;
+				}
+				/* We have a valid client certificate from the browser. */
+				$clientCert = str_replace(array("\r", "\n", " "), '', $matches[1]);
+
+				foreach ($scd->info as $thing) {
+				    if($thing instanceof SAML2_XML_ds_KeyInfo) {
+					$keyInfo[]=$thing;
+				    }
+				}
+				if (count($keyInfo)!=1) {
+				    $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:KeyInfo> element in <SubjectConfirmationData> allowed';
+				    continue;
+				}
+
+				foreach ($keyInfo[0]->info as $thing) {
+				    if($thing instanceof SAML2_XML_ds_X509Data) {
+					$x509data[]=$thing;
+				    }
+				}
+				if (count($x509data)!=1) {
+				    $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Data> element in <ds:KeyInfo> within <SubjectConfirmationData> allowed';
+				    continue;
+				}
+
+				foreach ($x509data[0]->data as $thing) {
+				    if($thing instanceof SAML2_XML_ds_X509Certificate) {
+					$x509cert[]=$thing;
+				    }
+				}
+				if (count($x509cert)!=1) {
+				    $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Certificate> element in <ds:X509Data> within <SubjectConfirmationData> allowed';
+				    continue;
+				}
+
+				$HoKCertificate = $x509cert[0]->certificate;
+				if ($HoKCertificate !== $clientCert) {
+					$lastError = 'Provided client certificate does not match the certificate bound to the Holder-of-Key assertion';
+					continue;
+				}
+			}
+
 			if ($scd->NotBefore && $scd->NotBefore > time() + 60) {
 				$lastError = 'NotBefore in SubjectConfirmationData is in the future: ' . $scd->NotBefore;
 				continue;
diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php
index 1f0d31bc301ecbea6222b9992b5bad2b3fb7b3e3..b19a392193cd3e95757dbc56a6ded8e8e424240b 100644
--- a/modules/saml/www/sp/metadata.php
+++ b/modules/saml/www/sp/metadata.php
@@ -72,6 +72,13 @@ $acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01';
 $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId . '/artifact');
 $sp->AssertionConsumerService[] = $acs;
 
+$acs = new SAML2_XML_md_IndexedEndpointType();
+$acs->index = 4;
+$acs->Binding = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser';
+$acs->ProtocolBinding = SAML2_Const::BINDING_HTTP_POST;
+$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId);
+$sp->AssertionConsumerService[] = $acs;
+
 $keys = array();
 $certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig, FALSE, 'new_');
 if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) {
diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php
index 3821cb2feeeddddc90557c573f753cf57ee20f22..e9ee76513211920742632a4cae8116e0f7d6a83b 100644
--- a/www/saml2/idp/metadata.php
+++ b/www/saml2/idp/metadata.php
@@ -60,7 +60,9 @@ try {
 	$metaArray = array(
 		'metadata-set' => 'saml20-idp-remote',
 		'entityid' => $idpentityid,
-		'SingleSignOnService' => $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'),
+		'SingleSignOnService' => array(0 => array(
+					'Binding' => SAML2_Const::BINDING_HTTP_REDIRECT,
+					'Location' => $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'))),
 		'SingleLogoutService' => $metadata->getGenerated('SingleLogoutService', 'saml20-idp-hosted'),
 	);
 
@@ -79,6 +81,14 @@ try {
 		);
 	}
 
+	if ($idpmeta->getBoolean('saml20.hok.assertion', FALSE)) {
+		/* Prepend HoK SSO Service endpoint. */
+		array_unshift($metaArray['SingleSignOnService'], array(
+			'hoksso:ProtocolBinding' => SAML2_Const::BINDING_HTTP_REDIRECT,
+			'Binding' => SAML2_Const::BINDING_HOK_SSO,
+			'Location' => SimpleSAML_Utilities::getBaseURL() . 'saml2/idp/SSOService.php'));
+	}
+
 	$metaArray['NameIDFormat'] = $idpmeta->getString('NameIDFormat', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient');
 
 	if ($idpmeta->hasValue('OrganizationName')) {