Otentikasi sertifikat klien HTTPS Java


222

Saya cukup baru di sini HTTPS/SSL/TLSdan saya agak bingung tentang apa yang seharusnya disajikan oleh klien saat mengautentikasi dengan sertifikat.

Saya sedang menulis klien Java yang perlu melakukan POSTdata sederhana untuk tertentu URL. Bagian itu berfungsi dengan baik, satu-satunya masalah adalah itu seharusnya selesai HTTPS. Bagian HTTPSini cukup mudah untuk ditangani (baik dengan HTTPclientatau menggunakan HTTPSdukungan bawaan Java ), tetapi saya terjebak pada otentikasi dengan sertifikat klien. Saya perhatikan sudah ada pertanyaan yang sangat mirip di sini, yang belum saya coba dengan kode saya (akan segera melakukannya). Masalah saya saat ini adalah - apa pun yang saya lakukan - klien Java tidak pernah mengirim sertifikat (saya bisa memeriksanya dengan PCAPdump).

Saya ingin tahu persis apa yang seharusnya dihadirkan klien ke server saat mengautentikasi dengan sertifikat (khusus untuk Java - jika itu penting)? Apakah ini JKSfile, atau PKCS#12? Apa yang seharusnya ada di dalamnya; hanya sertifikat klien, atau kunci? Jika demikian, kunci yang mana? Ada sedikit kebingungan tentang semua jenis file, tipe sertifikat, dan sebagainya.

Seperti yang telah saya katakan sebelumnya, saya baru HTTPS/SSL/TLSjadi saya akan menghargai beberapa informasi latar belakang juga (tidak harus menjadi esai; saya akan menerima tautan ke artikel bagus).


saya telah memberikan dua sertifikat dari klien bagaimana mengidentifikasi mana yang perlu ditambahkan keystore dan truststore, bisakah Anda membantu mengidentifikasi masalah ini karena Anda telah mengalami masalah serupa, masalah ini yang saya angkat sebenarnya tidak mendapatkan petunjuk apa yang harus dilakukan stackoverflow .com / pertanyaan / 61374276 / ...
henrycharles

Jawaban:


233

Akhirnya berhasil menyelesaikan semua masalah, jadi saya akan menjawab pertanyaan saya sendiri. Ini adalah pengaturan / file yang saya gunakan untuk mengelola untuk menyelesaikan masalah khusus saya;

The keystore klien adalah PKCS # 12 Format file yang berisi

  1. Publik klienSertifikat (dalam hal ini ditandatangani oleh CA yang ditandatangani sendiri)
  2. Kunci pribadi klien

Untuk menghasilkannya saya menggunakan pkcs12perintah OpenSSL , misalnya;

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"

Tip: pastikan Anda mendapatkan OpenSSL terbaru, bukan versi 0.9.8h karena sepertinya ada bug yang tidak memungkinkan Anda menghasilkan file PKCS # 12 dengan benar.

File PKCS # 12 ini akan digunakan oleh klien Java untuk menyajikan sertifikat klien ke server ketika server telah secara eksplisit meminta klien untuk mengotentikasi. Lihat artikel Wikipedia tentang TLS untuk ikhtisar tentang bagaimana protokol untuk otentikasi sertifikat klien benar-benar bekerja (juga menjelaskan mengapa kita memerlukan kunci pribadi klien di sini).

The truststore klien adalah lurus ke depan Format JKS file yang berisi akar atau CA menengah sertifikat . Sertifikat CA ini akan menentukan titik akhir mana Anda akan diizinkan untuk berkomunikasi dengan, dalam hal ini akan memungkinkan klien Anda untuk terhubung ke server mana saja yang menyajikan sertifikat yang ditandatangani oleh salah satu CA truststore.

Untuk menghasilkannya, Anda dapat menggunakan keytool Java standar, misalnya;

keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever
keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca

Menggunakan truststore ini, klien Anda akan mencoba melakukan jabat tangan SSL lengkap dengan semua server yang menyajikan sertifikat yang ditandatangani oleh CA yang diidentifikasi oleh myca.crt.

File-file di atas hanya untuk klien. Ketika Anda ingin mengatur server juga, server memerlukan file key- dan truststore sendiri. Sebuah walk-through yang bagus untuk menyiapkan contoh yang berfungsi penuh untuk klien Java dan server (menggunakan Tomcat) dapat ditemukan di situs web ini .

Masalah / Komentar / Kiat

  1. Otentikasi sertifikat klien hanya dapat diberlakukan oleh server.
  2. ( Penting! ) Ketika server meminta sertifikat klien (sebagai bagian dari jabat tangan TLS), server juga akan memberikan daftar CA tepercaya sebagai bagian dari permintaan sertifikat. Ketika sertifikat klien yang ingin Anda sajikan untuk otentikasi tidak ditandatangani oleh salah satu CA ini, itu tidak akan disajikan sama sekali (menurut saya, ini adalah perilaku aneh, tapi saya yakin ada alasan untuk itu). Ini adalah penyebab utama masalah saya, karena pihak lain tidak mengonfigurasi server mereka dengan baik untuk menerima sertifikat klien yang saya tandatangani sendiri dan kami berasumsi bahwa masalahnya ada pada saya karena tidak menyediakan sertifikat klien dengan benar dalam permintaan.
  3. Dapatkan Wireshark. Ini memiliki analisis paket SSL / HTTPS yang hebat dan akan sangat membantu debugging dan menemukan masalah. Ini mirip dengan -Djavax.net.debug=ssltetapi lebih terstruktur dan (bisa dibilang) lebih mudah untuk ditafsirkan jika Anda tidak nyaman dengan output debug SSL Java.
  4. Sangat mungkin untuk menggunakan perpustakaan Apache httpclient. Jika Anda ingin menggunakan httpclient, ganti saja URL tujuan dengan yang setara dengan HTTPS dan tambahkan argumen JVM berikut (yang sama untuk klien lain, terlepas dari pustaka yang ingin Anda gunakan untuk mengirim / menerima data melalui HTTP / HTTPS) :

    -Djavax.net.debug=ssl
    -Djavax.net.ssl.keyStoreType=pkcs12
    -Djavax.net.ssl.keyStore=client.p12
    -Djavax.net.ssl.keyStorePassword=whatever
    -Djavax.net.ssl.trustStoreType=jks
    -Djavax.net.ssl.trustStore=client-truststore.jks
    -Djavax.net.ssl.trustStorePassword=whatever

6
"Ketika sertifikat klien yang ingin Anda sajikan untuk otentikasi tidak ditandatangani oleh salah satu CA ini, itu tidak akan disajikan sama sekali". Sertifikat tidak disajikan karena klien tahu mereka tidak akan diterima oleh server. Selain itu, sertifikat Anda dapat ditandatangani oleh CA "ICA" perantara, dan server dapat memberikan root CA "RCA" kepada klien Anda, dan browser web Anda masih akan membiarkan Anda memilih sertifikat Anda meskipun itu ditandatangani oleh ICA bukan RCA.
KyleM

2
Sebagai contoh dari komentar di atas, pertimbangkan situasi di mana Anda memiliki satu root CA (RCA1) dan dua CA menengah (ICA1 dan ICA2). Di Apache Tomcat jika Anda mengimpor RCA1 ke trust store, browser web Anda akan menampilkan SEMUA sertifikat yang ditandatangani oleh ICA1 dan ICA2, meskipun mereka tidak ada di trust store Anda. Ini karena rantai yang penting bukan sertifikat individu.
KyleM

2
"Menurut saya, ini perilaku yang aneh, tapi saya yakin ada alasannya". Alasannya adalah karena itulah yang tertulis di RFC 2246. Tidak ada yang aneh tentang itu. Mengizinkan klien menampilkan sertifikat yang tidak akan diterima oleh server adalah hal yang aneh, dan benar-benar membuang waktu dan ruang.
Marquis of Lorne

1
Saya sangat merekomendasikan untuk tidak menggunakan tunggal, JVM-wide keystore (dan truststore!) Seperti yang Anda miliki dalam contoh Anda di atas. Menyesuaikan satu koneksi lebih aman dan lebih fleksibel, tetapi memang mengharuskan Anda untuk menulis sedikit lebih banyak kode. Anda harus menyesuaikan SSLContextseperti pada jawaban @ Magnus.
Christopher Schultz

63

Jawaban lain menunjukkan cara mengkonfigurasi sertifikat klien secara global. Namun jika Anda ingin secara program menentukan kunci klien untuk satu koneksi tertentu, alih-alih mendefinisikannya secara global di setiap aplikasi yang berjalan pada JVM Anda, maka Anda dapat mengonfigurasi SSLContext Anda sendiri seperti:

String keyPassphrase = "";

KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray());

SSLContext sslContext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, null)
        .build();

HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
HttpResponse response = httpClient.execute(new HttpGet("https://example.com"));

Saya harus menggunakan sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. Saya tidak bisa membuatnya bekerja loadKeyMaterial(...).
Conor Svensson

4
@ConorSvensson Trust material adalah untuk klien yang mempercayai sertifikat server jarak jauh, materi utama adalah untuk server yang mempercayai klien.
Magnus

1
Saya sangat suka jawaban singkat dan langsung ini. Jika orang tertarik, saya memberikan contoh yang berfungsi di sini dengan instruksi pembuatan. stackoverflow.com/a/46821977/154527
Alain O'Dea

3
Kamu menyelamatkan hariku! Hanya satu perubahan tambahan yang harus saya lakukan, kirimkan kata sandi dalam loadKeyMaterial(keystore, keyPassphrase.toCharArray())kode 'juga!
AnandShanbhag

1
@ Peterh Ya itu khusus untuk Apache http. Setiap perpustakaan HTTP akan memiliki caranya sendiri untuk dikonfigurasi, tetapi sebagian besar dari mereka entah bagaimana harus menggunakan SSLContext.
Magnus

30

File JKS mereka hanyalah wadah untuk sertifikat dan pasangan kunci. Dalam skenario otentikasi sisi klien, berbagai bagian tombol akan ditempatkan di sini:

  • Toko klien akan berisi pasangan kunci pribadi dan publik klien . Itu disebut keystore .
  • Toko server akan berisi kunci publik klien . Ini disebut truststore .

Pemisahan truststore dan keystore tidak wajib tetapi disarankan. Mereka dapat berupa file fisik yang sama.

Untuk mengatur lokasi sistem file dari dua toko, gunakan properti sistem berikut:

-Djavax.net.ssl.keyStore=clientsidestore.jks

dan di server:

-Djavax.net.ssl.trustStore=serversidestore.jks

Untuk mengekspor sertifikat klien (kunci publik) ke file, sehingga Anda dapat menyalinnya ke server, gunakan

keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks

Untuk mengimpor kunci publik klien ke keystore server, gunakan (seperti yang disebutkan poster, ini sudah dilakukan oleh admin server)

keytool -import -file publicclientkey.cer -store serversidestore.jks

Saya mungkin harus menyebutkan bahwa saya tidak punya kendali atas server. Server telah mengimpor sertifikat publik kami. Saya telah diberitahu oleh admin dari sistem itu bahwa saya perlu memberikan sertifikat secara eksplisit sehingga dapat dikirim bersama selama jabat tangan (server mereka secara eksplisit meminta ini).
tmbrggmn

Anda akan memerlukan kunci publik dan pribadi untuk sertifikat publik Anda (yang diketahui oleh server) sebagai file JKS.
sfussenegger

Terima kasih untuk kode contohnya. Dalam kode di atas, apa sebenarnya "mykey-public.cer"? Apakah ini sertifikat publik klien (kami menggunakan sertifikat yang ditandatangani sendiri)?
tmbrggmn

Ya itu, saya telah mengganti nama file dalam potongan kode sesuai. Saya harap ini membuatnya jelas.
mhaller

Benar, terima kasih. Saya terus bingung karena ternyata "kunci" dan "sertifikat" digunakan secara bergantian.
tmbrggmn

10

Maven pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>some.examples</groupId>
    <artifactId>sslcliauth</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sslcliauth</name>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4</version>
        </dependency>
    </dependencies>
</project>

Kode Java:

package some.examples;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.InputStreamEntity;

public class SSLCliAuthExample {

private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName());

private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS";
private static final String CA_KEYSTORE_PATH = "./cacert.jks";
private static final String CA_KEYSTORE_PASS = "changeit";

private static final String CLIENT_KEYSTORE_TYPE = "PKCS12";
private static final String CLIENT_KEYSTORE_PATH = "./client.p12";
private static final String CLIENT_KEYSTORE_PASS = "changeit";

public static void main(String[] args) throws Exception {
    requestTimestamp();
}

public final static void requestTimestamp() throws Exception {
    SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
            createSslCustomContext(),
            new String[]{"TLSv1"}, // Allow TLSv1 protocol only
            null,
            SSLConnectionSocketFactory.getDefaultHostnameVerifier());
    try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) {
        HttpPost req = new HttpPost("https://changeit.com/changeit");
        req.setConfig(configureRequest());
        HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin"));
        req.setEntity(ent);
        try (CloseableHttpResponse response = httpclient.execute(req)) {
            HttpEntity entity = response.getEntity();
            LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine());
            EntityUtils.consume(entity);
            LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString());
        }
    }
}

public static RequestConfig configureRequest() {
    HttpHost proxy = new HttpHost("changeit.local", 8080, "http");
    RequestConfig config = RequestConfig.custom()
            .setProxy(proxy)
            .build();
    return config;
}

public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
    // Trusted CA keystore
    KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE);
    tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray());

    // Client keystore
    KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE);
    cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray());

    SSLContext sslcontext = SSLContexts.custom()
            //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize
            .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate
            .build();
    return sslcontext;
}

}

Jika Anda lebih suka sertifikat tersedia untuk semua aplikasi yang menggunakan instalasi JVM tertentu, ikuti jawaban ini sebagai gantinya.
ADTC

Apakah metode configureRequest()untuk mengatur proksi proyek klien benar?
shareef

ya, ini adalah konfigurasi klien http dan ini adalah konfigurasi proxy dalam kasus ini
kinjelom

mungkin saya punya pertanyaan bodoh, tapi bagaimana cara membuat file cacert.jks? Saya memiliki pengecualian Pengecualian di utas "utama" java.io.FileNotFoundException:. \ Cacert.jks (Sistem tidak dapat menemukan file yang ditentukan)
Patlatus

6

Bagi Anda yang hanya ingin mengatur otentikasi dua arah (sertifikat server dan klien), kombinasi dari kedua tautan ini akan membawa Anda ke sana:

Pengaturan auth dua arah:

https://linuxconfig.org/apache-web-server-ssl-authentication

Anda tidak perlu menggunakan file konfigurasi openssl yang mereka sebutkan; gunakan saja

  • $ openssl genrsa -des3 -keluar ca.key 4096

  • $ openssl req -baru -x509-hari 365 -kunci ca.key -keluar ca.crt

untuk menghasilkan sertifikat CA Anda sendiri, dan kemudian menghasilkan dan menandatangani kunci server dan klien melalui:

  • $ openssl genrsa -des3 -out server.key 4096

  • $ openssl req -baru -kunci server.key -out server.csr

  • $ openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

dan

  • $ openssl genrsa -des3 -out client.key 4096

  • $ openssl req -baru -kunci client.key -keluar client.csr

  • $ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

Untuk sisanya ikuti langkah-langkah di tautan. Mengelola sertifikat untuk Chrome berfungsi sama seperti pada contoh untuk firefox yang disebutkan.

Selanjutnya, atur server melalui:

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04

Perhatikan bahwa Anda telah membuat server .crt dan .key sehingga Anda tidak perlu melakukan langkah itu lagi.


1
Pikirkan Anda memiliki kesalahan ketik dalam langkah pembuatan CSR server: sebaiknya server.keytidak menggunakanclient.key
the.Legend

0

Saya telah terhubung ke bank dengan SSL dua arah (sertifikat klien dan server) dengan Spring Boot. Jadi jelaskan di sini semua langkah saya, semoga membantu seseorang (solusi kerja paling sederhana, saya temukan):

  1. Buat permintaan sertifikat:

    • Hasilkan kunci pribadi:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • Buat permintaan sertifikat:

      openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD

    Simpan user.key(dan kata sandi) dan kirim permintaan sertifikat user.csrke bank untuk sertifikat saya

  2. Terima 2 sertifikat: sertifikat root klien saya clientId.crtdan sertifikat root bank:bank.crt

  3. Buat Java keystore (masukkan kata sandi kunci dan atur kata sandi keystore):

    openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root

    Tidak memperhatikan pada output: unable to write 'random state'. Java PKCS12 keystore.p12dibuat.

  4. Tambahkan ke keystore bank.crt(untuk kemudahan saya telah menggunakan satu keystore):

    keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops

    Periksa sertifikat keystore dengan:

    keytool -list -keystore keystore.p12
  5. Siap untuk kode Java :) Saya telah menggunakan Spring Boot RestTemplatedengan menambahkan org.apache.httpcomponents.httpcoreketergantungan:

    @Bean("sslRestTemplate")
    public RestTemplate sslRestTemplate() throws Exception {
      char[] storePassword = appProperties.getSslStorePassword().toCharArray();
      URL keyStore = new URL(appProperties.getSslStore());
    
      SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(keyStore, storePassword)
      // use storePassword twice (with key password do not work)!!
            .loadKeyMaterial(keyStore, storePassword, storePassword) 
            .build();
    
      // Solve "Certificate doesn't match any of the subject alternative names"
      SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    
      CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
      HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);
      RestTemplate restTemplate = new RestTemplate(factory);
      // restTemplate.setMessageConverters(List.of(new Jaxb2RootElementHttpMessageConverter()));
      return restTemplate;
    }

Anda dapat melakukan semuanya dengan keytool. Tidak perlu untuk OpenSSL dalam hal ini sama sekali.
Marquis of Lorne

0

Diberikan file p12 dengan sertifikat dan kunci privat (dihasilkan oleh openssl, misalnya), kode berikut akan menggunakannya untuk HttpsURLConnection tertentu:

    KeyStore keyStore = KeyStore.getInstance("pkcs12");
    keyStore.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keystorePassword.toCharArray());
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), null, null);
    SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();

    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setSSLSocketFactory(sslSocketFactory);

The SSLContextmengambil beberapa waktu untuk initialize, sehingga Anda mungkin ingin cache itu.


-1

Saya pikir perbaikan di sini adalah tipe keystore, pkcs12 (pfx) selalu memiliki kunci pribadi dan tipe JKS dapat ada tanpa kunci pribadi. Kecuali Anda menentukan dalam kode Anda atau memilih sertifikat melalui browser, server tidak memiliki cara untuk mengetahui itu mewakili klien di ujung yang lain.


1
Format PKCS12 secara tradisional digunakan untuk privatekey-AND-cert, tetapi Java sejak 8 tahun 2014 (lebih dari setahun sebelum jawaban ini) telah mendukung PKCS12 yang berisi cert (s) tanpa privatekey (s). Terlepas dari format keystore, otentikasi klien memang membutuhkan privatekey-AND-cert. Saya tidak mengerti kalimat kedua Anda, tetapi klien Java dapat secara otomatis memilih sertifikat dan kunci klien jika setidaknya satu entri yang sesuai tersedia atau keymanager dapat dikonfigurasi untuk menggunakan yang ditentukan.
dave_thompson_085

Kalimat kedua Anda sama sekali salah. Server menyediakan penandatangan tepercaya, dan klien meninggal atau tidak memberikan sertifikat yang membatasi itu. Secara otomatis, bukan melalui 'dalam kode Anda'. Itu adalah 'cara mengetahui server yang mewakili klien'.
Marquis of Lorne
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.