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
34/**
35 * An implementation of {@link CertPath} based on BoringSSL.
36 *
37 * @hide
38 */
39@Internal
40public class OpenSSLX509CertPath extends CertPath {
41    private static final long serialVersionUID = -3249106005255170761L;
42
43    private static final byte[] PKCS7_MARKER = new byte[] {
44            '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'P', 'K', 'C', 'S', '7'
45    };
46
47    private static final int PUSHBACK_SIZE = 64;
48
49    /**
50     * Supported encoding types for CerthPath. Used by the various APIs that
51     * encode this into bytes such as {@link #getEncoded()}.
52     */
53    private enum Encoding {
54        PKI_PATH("PkiPath"),
55        PKCS7("PKCS7");
56
57        private final String apiName;
58
59        Encoding(String apiName) {
60            this.apiName = apiName;
61        }
62
63        static Encoding findByApiName(String apiName) throws CertificateEncodingException {
64            for (Encoding element : values()) {
65                if (element.apiName.equals(apiName)) {
66                    return element;
67                }
68            }
69
70            return null;
71        }
72    }
73
74    /** Unmodifiable list of encodings for the API. */
75    private static final List<String> ALL_ENCODINGS = Collections.unmodifiableList(Arrays
76            .asList(new String[] {
77                    Encoding.PKI_PATH.apiName,
78                    Encoding.PKCS7.apiName,
79            }));
80
81    private static final Encoding DEFAULT_ENCODING = Encoding.PKI_PATH;
82
83    private final List<? extends X509Certificate> mCertificates;
84
85    static Iterator<String> getEncodingsIterator() {
86        return ALL_ENCODINGS.iterator();
87    }
88
89    protected OpenSSLX509CertPath(List<? extends X509Certificate> certificates) {
90        super("X.509");
91
92        mCertificates = certificates;
93    }
94
95    @Override
96    public List<? extends Certificate> getCertificates() {
97        return Collections.unmodifiableList(mCertificates);
98    }
99
100    private byte[] getEncoded(Encoding encoding) throws CertificateEncodingException {
101        final OpenSSLX509Certificate[] certs = new OpenSSLX509Certificate[mCertificates.size()];
102        final long[] certRefs = new long[certs.length];
103
104        for (int i = 0, j = certs.length - 1; j >= 0; i++, j--) {
105            final X509Certificate cert = mCertificates.get(i);
106
107            if (cert instanceof OpenSSLX509Certificate) {
108                certs[j] = (OpenSSLX509Certificate) cert;
109            } else {
110                certs[j] = OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
111            }
112
113            certRefs[j] = certs[j].getContext();
114        }
115
116        switch (encoding) {
117            case PKI_PATH:
118                return NativeCrypto.ASN1_seq_pack_X509(certRefs);
119            case PKCS7:
120                return NativeCrypto.i2d_PKCS7(certRefs);
121            default:
122                throw new CertificateEncodingException("Unknown encoding");
123        }
124    }
125
126    @Override
127    public byte[] getEncoded() throws CertificateEncodingException {
128        return getEncoded(DEFAULT_ENCODING);
129    }
130
131    @Override
132    public byte[] getEncoded(String encoding) throws CertificateEncodingException {
133        Encoding enc = Encoding.findByApiName(encoding);
134        if (enc == null) {
135            throw new CertificateEncodingException("Invalid encoding: " + encoding);
136        }
137
138        return getEncoded(enc);
139    }
140
141    @Override
142    public Iterator<String> getEncodings() {
143        return getEncodingsIterator();
144    }
145
146    private static CertPath fromPkiPathEncoding(InputStream inStream) throws CertificateException {
147        OpenSSLBIOInputStream bis = new OpenSSLBIOInputStream(inStream, true);
148
149        final boolean markable = inStream.markSupported();
150        if (markable) {
151            inStream.mark(PUSHBACK_SIZE);
152        }
153
154        final long[] certRefs;
155        try {
156            certRefs = NativeCrypto.ASN1_seq_unpack_X509_bio(bis.getBioContext());
157        } catch (Exception e) {
158            if (markable) {
159                try {
160                    inStream.reset();
161                } catch (IOException ignored) {
162                }
163            }
164            throw new CertificateException(e);
165        } finally {
166            bis.release();
167        }
168
169        if (certRefs == null) {
170            return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
171        }
172
173        final List<OpenSSLX509Certificate> certs =
174                new ArrayList<OpenSSLX509Certificate>(certRefs.length);
175        for (int i = certRefs.length - 1; i >= 0; i--) {
176            if (certRefs[i] == 0) {
177                continue;
178            }
179            certs.add(new OpenSSLX509Certificate(certRefs[i]));
180        }
181
182        return new OpenSSLX509CertPath(certs);
183    }
184
185    private static CertPath fromPkcs7Encoding(InputStream inStream) throws CertificateException {
186        try {
187            if (inStream == null || inStream.available() == 0) {
188                return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
189            }
190        } catch (IOException e) {
191            throw new CertificateException("Problem reading input stream", e);
192        }
193
194        final boolean markable = inStream.markSupported();
195        if (markable) {
196            inStream.mark(PUSHBACK_SIZE);
197        }
198
199        /* Attempt to see if this is a PKCS#7 bag. */
200        final PushbackInputStream pbis = new PushbackInputStream(inStream, PUSHBACK_SIZE);
201        try {
202            final byte[] buffer = new byte[PKCS7_MARKER.length];
203
204            final int len = pbis.read(buffer);
205            if (len < 0) {
206                /* No need to reset here. The stream was empty or EOF. */
207                throw new ParsingException("inStream is empty");
208            }
209            pbis.unread(buffer, 0, len);
210
211            if (len == PKCS7_MARKER.length && Arrays.equals(PKCS7_MARKER, buffer)) {
212                return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7PemInputStream(pbis));
213            }
214
215            return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7DerInputStream(pbis));
216        } catch (Exception e) {
217            if (markable) {
218                try {
219                    inStream.reset();
220                } catch (IOException ignored) {
221                }
222            }
223            throw new CertificateException(e);
224        }
225    }
226
227    private static CertPath fromEncoding(InputStream inStream, Encoding encoding)
228            throws CertificateException {
229        switch (encoding) {
230            case PKI_PATH:
231                return fromPkiPathEncoding(inStream);
232            case PKCS7:
233                return fromPkcs7Encoding(inStream);
234            default:
235                throw new CertificateEncodingException("Unknown encoding");
236        }
237    }
238
239    public static CertPath fromEncoding(InputStream inStream, String encoding)
240            throws CertificateException {
241        if (inStream == null) {
242            throw new CertificateException("inStream == null");
243        }
244
245        Encoding enc = Encoding.findByApiName(encoding);
246        if (enc == null) {
247            throw new CertificateException("Invalid encoding: " + encoding);
248        }
249
250        return fromEncoding(inStream, enc);
251    }
252
253    public static CertPath fromEncoding(InputStream inStream) throws CertificateException {
254        if (inStream == null) {
255            throw new CertificateException("inStream == null");
256        }
257
258        return fromEncoding(inStream, DEFAULT_ENCODING);
259    }
260}
261