001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2016 ForgeRock AS.
015 */
016
017package org.forgerock.openam.sts.user.invocation;
018
019import com.sun.identity.shared.encode.Base64;
020import org.forgerock.json.JsonValue;
021import org.forgerock.json.resource.ResourceException;
022import org.forgerock.openam.sts.AMSTSConstants;
023import org.forgerock.openam.sts.TokenMarshalException;
024
025import java.io.ByteArrayInputStream;
026import java.io.UnsupportedEncodingException;
027import java.security.cert.CertificateEncodingException;
028import java.security.cert.CertificateException;
029import java.security.cert.CertificateFactory;
030import java.security.cert.X509Certificate;
031
032import static org.forgerock.json.JsonValue.field;
033import static org.forgerock.json.JsonValue.json;
034import static org.forgerock.json.JsonValue.object;
035
036/**
037 * When issuing SAML2 Holder-of-Key assertions, the proof token is usually an X509Certificate. This state must be
038 * specified in the invocation, both to the REST-STS, and in the call to the TokenGenerationService made by the
039 * REST/SOAP STS. This is the analogue to the UseKey element in the WS-Trust defined RequestSecurityToken, which is
040 * defined as 'generally used when the client supplies a public-key that it wishes to be embedded in T as the proof key.'
041 * See http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/errata01/os/ws-trust-1.4-errata01-os-complete.html for details.
042 * The CXF-STS parses out the KeyInfo element included in the UseKey to create the org.apache.cxf.sts.request.ReceivedKey
043 * which encapsulates this public key. Thus the SOAP-STS can use this ReceivedKey to constitute the ProofTokenState,
044 * and the REST-STS will be invoked with the json representation of this class, which can then be forwarded on to the
045 * TokenGenerationService when SAML2 HoK tokens are being issued.
046 *
047 * Note that the WS-Trust spec allows for the UseKey to include symmetric key information, resulting in a SAML2 HoK with
048 * a KeyInfo element which contains symmetric key information. The TokenGenerationService and the REST-STS will not
049 * support proof tokens based on symmetric key information for the moment.
050 *
051 * It may be that PublicKey based proof tokens need to be supported in the future. If so, this class will add a ctor
052 * which takes a PublicKey, and encode which sort of proof-token-state has been provided (e.g. X509Certificate or PublicKey).
053 *
054 * @supported.all.api
055 */
056public class ProofTokenState {
057
058    /**
059     * Builder class for {@code ProofTokenState}
060     */
061    public static class ProofTokenStateBuilder {
062        private X509Certificate certificate;
063
064        private ProofTokenStateBuilder() {}
065
066        /**
067         * Adds an {@code X509Certificate} to the builder.
068         *
069         * @param certificate the certificate to add.
070         * @return the builder encapsulating the specified {@code X509Certificate}
071         */
072        public ProofTokenStateBuilder x509Certificate(X509Certificate certificate) {
073            this.certificate = certificate;
074            return this;
075        }
076
077        /**
078         * Builds the {@code ProofTokenState}
079         * @return the {@code ProofTokenState}
080         * @throws TokenMarshalException
081         */
082        public ProofTokenState build() throws TokenMarshalException {
083            return new ProofTokenState(this);
084        }
085    }
086
087    private static final String BASE_64_ENCODED_CERTIFICATE = "base64EncodedCertificate";
088    private static final String X_509 = "X.509";
089
090    private final X509Certificate certificate;
091
092    private ProofTokenState(ProofTokenStateBuilder builder) throws TokenMarshalException {
093        certificate = builder.certificate;
094        if (certificate == null) {
095            throw new TokenMarshalException(ResourceException.BAD_REQUEST, "the X509Certificate state must be set.");
096        }
097    }
098
099    /**
100     * Gets the {@code X509Certificate}
101     * @return the {@code X509Certificate}
102     */
103    public X509Certificate getX509Certificate() {
104        return certificate;
105    }
106
107    @Override
108    public boolean equals(Object other) {
109        if (other instanceof ProofTokenState) {
110            ProofTokenState otherState = (ProofTokenState)other;
111            return (certificate.equals(otherState.certificate));
112        }
113        return false;
114    }
115
116    @Override
117    public int hashCode() {
118        return certificate.hashCode();
119    }
120
121    @Override
122    public String toString() {
123        return toJson().toString();
124    }
125
126    /**
127     * Creates a {@code ProofTokenStateBuilder}
128     * @return a {@code ProofTokenStateBuilder}
129     */
130    public static ProofTokenStateBuilder builder() {
131        return new ProofTokenStateBuilder();
132    }
133
134    /**
135     * Constructs a {@code ProofTokenState} from the specified {@code JsonValue} representation.
136     *
137     * @param jsonValue the {@code JsonValue} representation to construct the {@code ProofTokenState} from.
138     * @return a {@code ProofTokenState}
139     * @throws TokenMarshalException
140     */
141    public static ProofTokenState fromJson(JsonValue jsonValue) throws TokenMarshalException {
142        final String certString = jsonValue.get(BASE_64_ENCODED_CERTIFICATE).asString();
143        try {
144            final X509Certificate x509Certificate = (X509Certificate)CertificateFactory.getInstance(X_509).generateCertificate(
145                    new ByteArrayInputStream(Base64.decode(certString.getBytes(AMSTSConstants.UTF_8_CHARSET_ID))));
146            return ProofTokenState.builder().x509Certificate(x509Certificate).build();
147        } catch (CertificateException e) {
148            throw new TokenMarshalException(ResourceException.BAD_REQUEST,
149                    "Exception caught marshalling from json to X509 cert: " + e, e);
150        } catch (UnsupportedEncodingException e) {
151            throw new TokenMarshalException(ResourceException.BAD_REQUEST,
152                    "Exception caught marshalling from json to X509 cert: " + e, e);
153        }
154    }
155
156    /**
157     * Gets the {@code JsonValue} representation of the {@code ProofTokenState}
158     * @return the {@code JsonValue} of the {@code ProofTokenState}
159     * @throws IllegalStateException
160     */
161    public JsonValue toJson() throws IllegalStateException {
162        try {
163            String base64EncodedCertificate = Base64.encode(certificate.getEncoded());
164            return json(object(field(BASE_64_ENCODED_CERTIFICATE, base64EncodedCertificate)));
165        } catch (CertificateEncodingException e) {
166            throw new IllegalStateException("Exception getting base64 representation of certificate: " + e, e);
167        }
168    }
169}