Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support system trusted certificates #58

Merged
merged 5 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ Works only with the combined option while only specifying a single url.
crip export pem -u=https://github.com --combined=true --destination=/path/to/export/github-chain.crt
```

### Extract system certificates
```bash
crip export pem -u=system
```

## Contributing

There are plenty of ways to contribute to this project:
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/nl/altindag/crip/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@
package nl.altindag.crip;

import nl.altindag.crip.command.CertificateRipper;
import nl.altindag.crip.provider.CertificateRipperProvider;
import nl.altindag.crip.util.HelpFactory;
import picocli.CommandLine;

import java.security.Security;

public class App {

public static void main(String[] applicationArguments) {
// Temporally ignoring KeychainStore as it does not work with Graal VM yet.
// The actual call to get the KeychainStore from the Apple Provider will be intercepted, and it will return a dummy keystore
// See here for the related issue https://github.com/oracle/graal/issues/10387
Security.insertProviderAt(new CertificateRipperProvider(), 1);

new CommandLine(new CertificateRipper())
.setCaseInsensitiveEnumValuesAllowed(true)
.setHelpFactory(new HelpFactory())
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/nl/altindag/crip/command/SharedProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
@SuppressWarnings({"unused", "FieldCanBeLocal", "FieldMayBeFinal"})
public class SharedProperties {

private static final String SYSTEM = "system";

@Option(names = {"-u", "--url"}, description = "Url of the target server to extract the certificates", required = true)
private List<String> urls = new ArrayList<>();
private List<String> uniqueUrls;
Expand Down Expand Up @@ -70,6 +72,15 @@ public CertificateHolder getCertificateHolder() {
.map(url -> new AbstractMap.SimpleEntry<>(url, client.get(url)))
.collect(Collectors.collectingAndThen(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (key1, key2) -> key1, LinkedHashMap::new), HashMap::new));

if (urls.contains(SYSTEM)) {
try {
List<X509Certificate> systemTrustedCertificates = CertificateUtils.getSystemTrustedCertificates();
urlsToCertificates.put(SYSTEM, systemTrustedCertificates);
} catch (UnsatisfiedLinkError error) {
System.out.printf("Unable to extract system certificates for %s\n", System.getProperty("os.name"));
}
}

return new CertificateHolder(urlsToCertificates);
}

Expand Down Expand Up @@ -100,6 +111,10 @@ public List<String> getUrls() {
Map<String, List<Integer>> hostToPort = new HashMap<>();

for (String url : urls) {
if (SYSTEM.equals(url)) {
continue;
}

String host = UriUtils.extractHost(url);
int port = UriUtils.extractPort(url);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2021 Thunderberry.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nl.altindag.crip.provider;

import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.Provider;
import java.security.ProviderException;

public final class CertificateRipperProvider extends Provider {

private static final class MockAppleProviderService extends Provider.Service {

public MockAppleProviderService(Provider p, String type, String algo, String cn) {
super(p, type, algo, cn, null, null);
}

@Override
public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException {
String type = getType();
String algo = getAlgorithm();
try {
if (type.equals("KeyStore") && algo.equals("KeychainStore") || algo.equals("KeychainStore-ROOT")) {
return new DummyKeychainStore();
}
} catch (Exception ex) {
throw new NoSuchAlgorithmException("Error constructing " + type + " for " + algo, ex);
}
throw new ProviderException("No impl for " + algo + " " + type);
}
}

public CertificateRipperProvider() {
super("CertificateRipper", 1.0, "Certificate Ripper Security Provider");

final Provider provider = this;
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
putService(new MockAppleProviderService(provider, "KeyStore", "KeychainStore", "apple.security.KeychainStore$USER"));
putService(new MockAppleProviderService(provider, "KeyStore", "KeychainStore-ROOT", "apple.security.KeychainStore$ROOT"));
return null;
});
}

}
109 changes: 109 additions & 0 deletions src/main/java/nl/altindag/crip/provider/DummyKeychainStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2021 Thunderberry.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nl.altindag.crip.provider;

import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStoreSpi;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;

final class DummyKeychainStore extends KeyStoreSpi {

@Override
public Key engineGetKey(String alias, char[] password) {
return null;
}

@Override
public Certificate[] engineGetCertificateChain(String alias) {
return new Certificate[0];
}

@Override
public Certificate engineGetCertificate(String alias) {
return null;
}

@Override
public Date engineGetCreationDate(String alias) {
return null;
}

@Override
public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) {

}

@Override
public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) {

}

@Override
public void engineSetCertificateEntry(String alias, Certificate cert) {

}

@Override
public void engineDeleteEntry(String alias) {

}

@Override
public Enumeration<String> engineAliases() {
return Collections.emptyEnumeration();
}

@Override
public boolean engineContainsAlias(String alias) {
return false;
}

@Override
public int engineSize() {
return 0;
}

@Override
public boolean engineIsKeyEntry(String alias) {
return false;
}

@Override
public boolean engineIsCertificateEntry(String alias) {
return false;
}

@Override
public String engineGetCertificateAlias(Certificate cert) {
return "";
}

@Override
public void engineStore(OutputStream stream, char[] password) {

}

@Override
public void engineLoad(InputStream stream, char[] password) {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,19 @@ void timeoutWhenServerTakesToLongToRespond() throws IOException {
logCaptor.close();
}

@Test
void processSystemTrustedCertificates() throws IOException {
createTempDirAndClearConsoleCaptor();

cmd.execute("export", "der", "--url=system", "--destination=" + TEMP_DIRECTORY.toAbsolutePath());

List<Path> files = Files.walk(TEMP_DIRECTORY, 1)
.filter(Files::isRegularFile)
.collect(Collectors.toList());

assertThat(files)
.hasSizeGreaterThan(1)
.allMatch(path -> path.toString().endsWith(".crt"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,19 @@ void getKeyStoreType() {
assertThat(keyStoreType).isEqualTo("JKS");
}

@Test
void processSystemTrustedCertificates() throws IOException {
createTempDirAndClearConsoleCaptor();

cmd.execute("export", "jks", "--url=system", "--destination=" + TEMP_DIRECTORY.toAbsolutePath().resolve("my-truststore.jks"));

List<Path> files = Files.walk(TEMP_DIRECTORY, 1)
.filter(Files::isRegularFile)
.collect(Collectors.toList());

assertThat(files)
.hasSize(1)
.allMatch(path -> path.toString().endsWith(".jks"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,19 @@ void resolveRootCaOnlyWhenEnabled() throws IOException {
}
}

@Test
void processSystemTrustedCertificates() throws IOException {
createTempDirAndClearConsoleCaptor();

cmd.execute("export", "pem", "--url=system", "--combined=true", "--destination=" + TEMP_DIRECTORY.toAbsolutePath());

List<Path> files = Files.walk(TEMP_DIRECTORY, 1)
.filter(Files::isRegularFile)
.collect(Collectors.toList());

assertThat(files)
.hasSize(1)
.allMatch(path -> path.toString().endsWith(".crt"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,19 @@ void getKeyStoreType() {
assertThat(keyStoreType).isEqualTo("PKCS12");
}

@Test
void processSystemTrustedCertificates() throws IOException {
createTempDirAndClearConsoleCaptor();

cmd.execute("export", "p12", "--url=system", "--destination=" + TEMP_DIRECTORY.toAbsolutePath().resolve("my-truststore.p12"));

List<Path> files = Files.walk(TEMP_DIRECTORY, 1)
.filter(Files::isRegularFile)
.collect(Collectors.toList());

assertThat(files)
.hasSize(1)
.allMatch(path -> path.toString().endsWith(".p12"));
}

}