Skip to content

Commit f11746a

Browse files
Merge pull request #239 from iamjpotts/20221107-certificate-authority
Add Config::add_root_certificate for trusting custom ca
2 parents 6c4ac77 + 5c7dedd commit f11746a

17 files changed

+402
-8
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ tag-message = "dkregistry v{{version}}"
2525
[dependencies]
2626
base64 = "0.13"
2727
futures = "0.3"
28-
http = "0.2"
2928

3029
# Pin libflate <1.3.0
3130
# https://github.com/sile/libflate/commit/aba829043f8a2d527b6c4984034fbe5e7adb0da6
@@ -55,7 +54,9 @@ url = "2.1.1"
5554
[dev-dependencies]
5655
dirs = "4.0"
5756
env_logger = "0.8"
57+
hyper = "0.14.28"
5858
mockito = "0.30"
59+
native-tls = "0.2"
5960
spectral = "0.6"
6061
test-case = "1.0.0"
6162
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }

certificate/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Don't commit the binaries; they are only needed to occasionally regenerate the certificates.
2+
cfssl
3+
cfssljson

certificate/README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
# Certificate Generation and Persistence for Tests
3+
4+
While it is possible to automagically generate certificates using the [rcgen](https://github.com/est31/rcgen)
5+
crate, that library (as of version 0.10.0) has a dependency on the [ring](https://github.com/briansmith/ring)
6+
crate, which has a non-trivial set of licenses.
7+
8+
To avoid potential problems with the licenses applying to `ring`, `rcgen` is not used to generate
9+
test certificates.
10+
11+
## Generating and Persisting Test Certificates
12+
13+
The tests require a self-signed certificate authority, and a private key / server certificate pair signed by
14+
that same CA.
15+
16+
Certificates are defined in json files, generated using [cfssl](https://github.com/cloudflare/cfssl), and
17+
committed into git.
18+
19+
### Install `cfssl` and Re-generate Certificates
20+
21+
$ ./download-cfssl.sh
22+
$ ./create-ca.sh
23+
$ ./create-localhost.sh
24+
25+
Note: You should not have to regenerate any certificates unless they expire, the ciphers become insecure,
26+
or the certificates otherwise become rejected by future versions of cryptography libraries.
27+
28+
### Definitions
29+
30+
* [profiles.json](profiles.json)
31+
* [ca.json](ca.json)
32+
* [localhost.json](localhost.json)

certificate/ca.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"CN": "Automated Testing CA",
3+
"key": {
4+
"algo": "rsa",
5+
"size": 2048
6+
},
7+
"names": [
8+
{
9+
"C": "USA"
10+
}
11+
]
12+
}
13+

certificate/create-ca.sh

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
pushd output
6+
7+
../cfssl gencert \
8+
-config ../profiles.json \
9+
-initca ../ca.json \
10+
| ../cfssljson -bare ca
11+
12+
popd
13+

certificate/create-localhost.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
pushd output
6+
7+
../cfssl gencert \
8+
-ca ca.pem \
9+
-ca-key ca-key.pem \
10+
-config ../profiles.json \
11+
-profile=server \
12+
../localhost.json \
13+
| ../cfssljson -bare localhost
14+
15+
cat localhost.pem ca.pem > localhost.crt
16+
17+
openssl \
18+
pkcs8 \
19+
-topk8 \
20+
-inform PEM \
21+
-outform PEM \
22+
-nocrypt \
23+
-in localhost-key.pem \
24+
-out localhost-key-pkcs8.pem
25+
26+
popd
27+

certificate/download-cfssl.sh

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
version=1.6.1
4+
5+
rm -f cfssl
6+
wget -O cfssl https://github.com/cloudflare/cfssl/releases/download/v${version}/cfssl_${version}_linux_amd64
7+
chmod +x cfssl
8+
9+
rm -f cfssljson
10+
wget -O cfssljson https://github.com/cloudflare/cfssl/releases/download/v${version}/cfssljson_${version}_linux_amd64
11+
chmod +x cfssljson
12+

certificate/localhost.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"CN": "localhost",
3+
"key": {
4+
"algo": "ecdsa",
5+
"size": 256
6+
},
7+
"names": [
8+
{
9+
"C": "USA"
10+
}
11+
],
12+
"hosts": [
13+
"localhost"
14+
]
15+
}

certificate/output/.gitignore

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# For CA, only need public key after server certificate is created
2+
ca.csr
3+
ca-key.pem
4+
5+
# For server, only need private key and chained public key
6+
localhost.csr
7+
localhost.pem
8+
9+
# Throw away pkcs1 flavor and keep pkcs8 flavor
10+
localhost-key.pem
11+

certificate/output/ca.pem

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDKjCCAhKgAwIBAgIUIm0u2dDryGQArfPLSadKLGAJ+MAwDQYJKoZIhvcNAQEL
3+
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
4+
QTAeFw0yMjExMDgxMzE2MDBaFw0yNzExMDcxMzE2MDBaMC0xDDAKBgNVBAYTA1VT
5+
QTEdMBsGA1UEAxMUQXV0b21hdGVkIFRlc3RpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
6+
AQUAA4IBDwAwggEKAoIBAQDBxBtMTvxybYrSrPbka3xD+Pzoj7MG6Fldh5j2vPsw
7+
Nz+SFwIGvU8XeJcSKIcAwFBCJ/GkYF8Uoa1/l6AXvafn1SmtricV3AYxYq40vXL+
8+
P1WY2HlXP4pwjbMF6uPiOm5r5HBpK5uptgJZRxMhbdqtoJP1/Acbrn62DYy4eqZN
9+
i9f+eiVKewn7Z40TONigzNyz1J1ffH3fA18MmcrXGfWF0figbSL3XpSn4nu3R2mm
10+
0rVpPQf6E+OHRS2NF0ekN7Xn8oMCQYHOXme3V8i2Sth6jyv9bhlvAGGJVY6XgIKa
11+
URSIjm9M87S0bYzi3YSMP6p2rxmHV/gOxnZ3e0wBihivAgMBAAGjQjBAMA4GA1Ud
12+
DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR4TW/Tbj2b74Fi
13+
a2kaPkCKj+JZUjANBgkqhkiG9w0BAQsFAAOCAQEAnRIkllxB6vtIxoV7HYizdxEo
14+
biS5dB0ErqMYFkOOYyLA9RCgqaFNmEvwzxg+yE9AggGs3Me68hma8Oe+1iydGUjv
15+
Emhh3XK/0ZCKJ63071wBAr5I9kOzbtPytyF6gaxPtpqqUcp6WyE0snFQt/1Vq/S8
16+
AMxPvU60thYUR1xPSSaPa3cEHMcgC/O4DCjmoJaILlrNShqvPcV2QD75D+HjcK68
17+
EIKhqluRwZsh/LrH8btUgtl5nAPNFRe4QiEeLCJHGPZ29mBCSeQXTKeRaSQe3Ixp
18+
q/ObtXVbTanhQG5WAvxJAb7MdFD+N4q0C3D6HmlXL1g/zyMF9PTHRtCBjp7ytA==
19+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQeEZs5zOQRA/JcoZ
3+
eX9hLSUk9CNqbb3fmAhqn5f7q8WhRANCAAQhtz6gl0uLfATyk1B9AhFsXgHEDMpQ
4+
Poa0UUrNkJye3LZe6iGnCTWHAS3Qr/hecohUY6mNQplnufBtdAE9jJd1
5+
-----END PRIVATE KEY-----

certificate/output/localhost.crt

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICnzCCAYegAwIBAgIUG2PchR5jyYocFHa+6LWgMO1MTJIwDQYJKoZIhvcNAQEL
3+
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
4+
QTAeFw0yMjExMDgxMzE2MDBaFw0zMjExMDUxMzE2MDBaMCIxDDAKBgNVBAYTA1VT
5+
QTESMBAGA1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
6+
Ibc+oJdLi3wE8pNQfQIRbF4BxAzKUD6GtFFKzZCcnty2Xuohpwk1hwEt0K/4XnKI
7+
VGOpjUKZZ7nwbXQBPYyXdaOBjDCBiTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww
8+
CgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzBknP8vwv3KAPEcE
9+
q3OYho/q3FAwHwYDVR0jBBgwFoAUeE1v0249m++BYmtpGj5Aio/iWVIwFAYDVR0R
10+
BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCeQ4rwIp6wZBTZDflm
11+
0Olj4czaOfsLMhoTYVoarfAzB57uV1yP87kOMFHaMycLViZzPi+T1rOjDCIQWNLh
12+
h6EMoGDkPLNZSG2KxVRKnOFQgE50CPobgEGZFmAIuBNjHX7MG8I1J/HO0X9Krzz6
13+
wqdyy0IBtv64W7wrty2ab+okBiNPlgV1mxzWlRJk8zcPY/aLOkJ+5Gd40YQNtWAd
14+
dPPevJIF/Dh+OadvUXtkiwmoJzn6pWwFwzyTp9kcSYVZYo5LWzV5U6l/HJVFNq/f
15+
a3U1Grw2T4Nb33G1cGn5xfEqnMvaWEAmDK7bb/smY/dTocnUUD3FGBmkNMXqE4FK
16+
nC9q
17+
-----END CERTIFICATE-----
18+
-----BEGIN CERTIFICATE-----
19+
MIIDKjCCAhKgAwIBAgIUIm0u2dDryGQArfPLSadKLGAJ+MAwDQYJKoZIhvcNAQEL
20+
BQAwLTEMMAoGA1UEBhMDVVNBMR0wGwYDVQQDExRBdXRvbWF0ZWQgVGVzdGluZyBD
21+
QTAeFw0yMjExMDgxMzE2MDBaFw0yNzExMDcxMzE2MDBaMC0xDDAKBgNVBAYTA1VT
22+
QTEdMBsGA1UEAxMUQXV0b21hdGVkIFRlc3RpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
23+
AQUAA4IBDwAwggEKAoIBAQDBxBtMTvxybYrSrPbka3xD+Pzoj7MG6Fldh5j2vPsw
24+
Nz+SFwIGvU8XeJcSKIcAwFBCJ/GkYF8Uoa1/l6AXvafn1SmtricV3AYxYq40vXL+
25+
P1WY2HlXP4pwjbMF6uPiOm5r5HBpK5uptgJZRxMhbdqtoJP1/Acbrn62DYy4eqZN
26+
i9f+eiVKewn7Z40TONigzNyz1J1ffH3fA18MmcrXGfWF0figbSL3XpSn4nu3R2mm
27+
0rVpPQf6E+OHRS2NF0ekN7Xn8oMCQYHOXme3V8i2Sth6jyv9bhlvAGGJVY6XgIKa
28+
URSIjm9M87S0bYzi3YSMP6p2rxmHV/gOxnZ3e0wBihivAgMBAAGjQjBAMA4GA1Ud
29+
DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR4TW/Tbj2b74Fi
30+
a2kaPkCKj+JZUjANBgkqhkiG9w0BAQsFAAOCAQEAnRIkllxB6vtIxoV7HYizdxEo
31+
biS5dB0ErqMYFkOOYyLA9RCgqaFNmEvwzxg+yE9AggGs3Me68hma8Oe+1iydGUjv
32+
Emhh3XK/0ZCKJ63071wBAr5I9kOzbtPytyF6gaxPtpqqUcp6WyE0snFQt/1Vq/S8
33+
AMxPvU60thYUR1xPSSaPa3cEHMcgC/O4DCjmoJaILlrNShqvPcV2QD75D+HjcK68
34+
EIKhqluRwZsh/LrH8btUgtl5nAPNFRe4QiEeLCJHGPZ29mBCSeQXTKeRaSQe3Ixp
35+
q/ObtXVbTanhQG5WAvxJAb7MdFD+N4q0C3D6HmlXL1g/zyMF9PTHRtCBjp7ytA==
36+
-----END CERTIFICATE-----

certificate/profiles.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"signing": {
3+
"default": {
4+
"expiry": "87600h"
5+
},
6+
"profiles": {
7+
"server": {
8+
"usages": [
9+
"signing",
10+
"digital signing",
11+
"key encipherment",
12+
"server auth"
13+
],
14+
"expiry": "87600h"
15+
}
16+
}
17+
}
18+
}
19+

certificate/reset.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
pushd output
6+
7+
rm -f *.csr *.pem *.crt
8+
9+
popd

src/errors.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub enum Error {
66
#[error("base64 decode error")]
77
Base64Decode(#[from] base64::DecodeError),
88
#[error("header parse error")]
9-
HeaderParse(#[from] http::header::ToStrError),
9+
HeaderParse(#[from] reqwest::header::ToStrError),
1010
#[error("json error")]
1111
Json(#[from] serde_json::Error),
1212
#[error("http transport error: {0}")]
@@ -28,7 +28,7 @@ pub enum Error {
2828
#[error("missing authentication header {0}")]
2929
MissingAuthHeader(&'static str),
3030
#[error("unexpected HTTP status {0}")]
31-
UnexpectedHttpStatus(http::StatusCode),
31+
UnexpectedHttpStatus(reqwest::StatusCode),
3232
#[error("invalid auth token '{0}'")]
3333
InvalidAuthToken(String),
3434
#[error("API V2 not supported")]
@@ -38,9 +38,9 @@ pub enum Error {
3838
#[error("www-authenticate header parse error")]
3939
Www(#[from] crate::v2::WwwHeaderParseError),
4040
#[error("request failed with status {status}")]
41-
Client { status: http::StatusCode },
41+
Client { status: reqwest::StatusCode },
4242
#[error("request failed with status {status}")]
43-
Server { status: http::StatusCode },
43+
Server { status: reqwest::StatusCode },
4444
#[error("content digest error")]
4545
ContentDigestParse(#[from] crate::v2::ContentDigestError),
4646
#[error("no header Content-Type given and no workaround to apply")]

src/v2/config.rs

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{mediatypes::MediaTypes, v2::*};
2+
use reqwest::Certificate;
23

34
/// Configuration for a `Client`.
45
#[derive(Debug)]
@@ -9,6 +10,7 @@ pub struct Config {
910
username: Option<String>,
1011
password: Option<String>,
1112
accept_invalid_certs: bool,
13+
root_certificates: Vec<Certificate>,
1214
accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>,
1315
}
1416

@@ -31,6 +33,12 @@ impl Config {
3133
self
3234
}
3335

36+
/// Add a root certificate the client should trust for TLS verification
37+
pub fn add_root_certificate(mut self, certificate: Certificate) -> Self {
38+
self.root_certificates.push(certificate);
39+
self
40+
}
41+
3442
/// Set custom Accept headers
3543
pub fn accepted_types(
3644
mut self,
@@ -87,9 +95,15 @@ impl Config {
8795
p.unwrap_or_else(|| "".into()),
8896
)),
8997
};
90-
let client = reqwest::ClientBuilder::new()
91-
.danger_accept_invalid_certs(self.accept_invalid_certs)
92-
.build()?;
98+
99+
let mut builder =
100+
reqwest::ClientBuilder::new().danger_accept_invalid_certs(self.accept_invalid_certs);
101+
102+
for ca in self.root_certificates {
103+
builder = builder.add_root_certificate(ca)
104+
}
105+
106+
let client = builder.build()?;
93107

94108
let accepted_types = match self.accepted_types {
95109
Some(a) => a,
@@ -130,6 +144,7 @@ impl Default for Config {
130144
index: "registry-1.docker.io".into(),
131145
insecure_registry: false,
132146
accept_invalid_certs: false,
147+
root_certificates: Default::default(),
133148
accepted_types: None,
134149
user_agent: Some(crate::USER_AGENT.to_owned()),
135150
username: None,

0 commit comments

Comments
 (0)