1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.conscrypt;
18
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.PushbackInputStream;
22import java.security.cert.CertPath;
23import java.security.cert.Certificate;
24import java.security.cert.CertificateEncodingException;
25import java.security.cert.CertificateException;
26import java.security.cert.X509Certificate;
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.Collections;
30import java.util.Iterator;
31import java.util.List;
32import org.conscrypt.OpenSSLX509CertificateFactory.ParsingException;
33
34public class OpenSSLX509CertPath extends CertPath {
35    private static final byte[] PKCS7_MARKER = "-----BEGIN PKCS7".getBytes();
36
37    private static final int PUSHBACK_SIZE = 64;
38
39    /**
40     * Supported encoding types for CerthPath. Used by the various APIs that
41     * encode this into bytes such as {@link #getEncoded()}.
42     */
43    private enum Encoding {
44        PKI_PATH("PkiPath"),
45        PKCS7("PKCS7");
46
47        private final String apiName;
48
49        Encoding(String apiName) {
50            this.apiName = apiName;
51        }
52
53        static Encoding findByApiName(String apiName) throws CertificateEncodingException {
54            for (Encoding element : values()) {
55                if (element.apiName.equals(apiName)) {
56                    return element;
57                }
58            }
59
60            return null;
61        }
62    }
63
64    /** Unmodifiable list of encodings for the API. */
65    private static final List<String> ALL_ENCODINGS = Collections.unmodifiableList(Arrays
66            .asList(new String[] {
67                    Encoding.PKI_PATH.apiName,
68                    Encoding.PKCS7.apiName,
69            }));
70
71    private static final Encoding DEFAULT_ENCODING = Encoding.PKI_PATH;
72
73    private final List<? extends X509Certificate> mCertificates;
74
75    static Iterator<String> getEncodingsIterator() {
76        return ALL_ENCODINGS.iterator();
77    }
78
79    protected OpenSSLX509CertPath(List<? extends X509Certificate> certificates) {
80        super("X.509");
81
82        mCertificates = certificates;
83    }
84
85    @Override
86    public List<? extends Certificate> getCertificates() {
87        return Collections.unmodifiableList(mCertificates);
88    }
89
90    private byte[] getEncoded(Encoding encoding) throws CertificateEncodingException {
91        final OpenSSLX509Certificate[] certs = new OpenSSLX509Certificate[mCertificates.size()];
92        final long[] certRefs = new long[certs.length];
93
94        for (int i = 0, j = certs.length - 1; j >= 0; i++, j--) {
95            final X509Certificate cert = mCertificates.get(i);
96
97            if (cert instanceof OpenSSLX509Certificate) {
98                certs[j] = (OpenSSLX509Certificate) cert;
99            } else {
100                certs[j] = OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
101            }
102
103            certRefs[j] = certs[j].getContext();
104        }
105
106        switch (encoding) {
107            case PKI_PATH:
108                return NativeCrypto.ASN1_seq_pack_X509(certRefs);
109            case PKCS7:
110                return NativeCrypto.i2d_PKCS7(certRefs);
111            default:
112                throw new CertificateEncodingException("Unknown encoding");
113        }
114    }
115
116    @Override
117    public byte[] getEncoded() throws CertificateEncodingException {
118        return getEncoded(DEFAULT_ENCODING);
119    }
120
121    @Override
122    public byte[] getEncoded(String encoding) throws CertificateEncodingException {
123        Encoding enc = Encoding.findByApiName(encoding);
124        if (enc == null) {
125            throw new CertificateEncodingException("Invalid encoding: " + encoding);
126        }
127
128        return getEncoded(enc);
129    }
130
131    @Override
132    public Iterator<String> getEncodings() {
133        return getEncodingsIterator();
134    }
135
136    private static CertPath fromPkiPathEncoding(InputStream inStream) throws CertificateException {
137        OpenSSLBIOInputStream bis = new OpenSSLBIOInputStream(inStream);
138
139        final boolean markable = inStream.markSupported();
140        if (markable) {
141            inStream.mark(PUSHBACK_SIZE);
142        }
143
144        final long[] certRefs;
145        try {
146            certRefs = NativeCrypto.ASN1_seq_unpack_X509_bio(bis.getBioContext());
147        } catch (Exception e) {
148            if (markable) {
149                try {
150                    inStream.reset();
151                } catch (IOException ignored) {
152                }
153            }
154            throw new CertificateException(e);
155        } finally {
156            NativeCrypto.BIO_free(bis.getBioContext());
157        }
158
159        if (certRefs == null) {
160            return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
161        }
162
163        final List<OpenSSLX509Certificate> certs =
164                new ArrayList<OpenSSLX509Certificate>(certRefs.length);
165        for (int i = certRefs.length - 1; i >= 0; i--) {
166            if (certRefs[i] == 0) {
167                continue;
168            }
169            certs.add(new OpenSSLX509Certificate(certRefs[i]));
170        }
171
172        return new OpenSSLX509CertPath(certs);
173    }
174
175    private static CertPath fromPkcs7Encoding(InputStream inStream) throws CertificateException {
176        try {
177            if (inStream == null || inStream.available() == 0) {
178                return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
179            }
180        } catch (IOException e) {
181            throw new CertificateException("Problem reading input stream", e);
182        }
183
184        final boolean markable = inStream.markSupported();
185        if (markable) {
186            inStream.mark(PUSHBACK_SIZE);
187        }
188
189        /* Attempt to see if this is a PKCS#7 bag. */
190        final PushbackInputStream pbis = new PushbackInputStream(inStream, PUSHBACK_SIZE);
191        try {
192            final byte[] buffer = new byte[PKCS7_MARKER.length];
193
194            final int len = pbis.read(buffer);
195            if (len < 0) {
196                /* No need to reset here. The stream was empty or EOF. */
197                throw new ParsingException("inStream is empty");
198            }
199            pbis.unread(buffer, 0, len);
200
201            if (len == PKCS7_MARKER.length && Arrays.equals(PKCS7_MARKER, buffer)) {
202                return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7PemInputStream(pbis));
203            }
204
205            return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7DerInputStream(pbis));
206        } catch (Exception e) {
207            if (markable) {
208                try {
209                    inStream.reset();
210                } catch (IOException ignored) {
211                }
212            }
213            throw new CertificateException(e);
214        }
215    }
216
217    private static CertPath fromEncoding(InputStream inStream, Encoding encoding)
218            throws CertificateException {
219        switch (encoding) {
220            case PKI_PATH:
221                return fromPkiPathEncoding(inStream);
222            case PKCS7:
223                return fromPkcs7Encoding(inStream);
224            default:
225                throw new CertificateEncodingException("Unknown encoding");
226        }
227    }
228
229    public static CertPath fromEncoding(InputStream inStream, String encoding)
230            throws CertificateException {
231        if (inStream == null) {
232            throw new CertificateException("inStream == null");
233        }
234
235        Encoding enc = Encoding.findByApiName(encoding);
236        if (enc == null) {
237            throw new CertificateException("Invalid encoding: " + encoding);
238        }
239
240        return fromEncoding(inStream, enc);
241    }
242
243    public static CertPath fromEncoding(InputStream inStream) throws CertificateException {
244        return fromEncoding(inStream, DEFAULT_ENCODING);
245    }
246}
247