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

Custom X509 extensions #1411

Open
ipetr0v opened this issue Feb 2, 2021 · 5 comments
Open

Custom X509 extensions #1411

ipetr0v opened this issue Feb 2, 2021 · 5 comments

Comments

@ipetr0v
Copy link

ipetr0v commented Feb 2, 2021

Currently rust-openssl supports a number of X509 extensions, though it's not a complete list. And if I understand correctly it doesn't support custom extensions.

I think it would be great to support creating custom X509 extensions, similar to how it's done in openssl:

X509 *x;
int nid;
nid = OBJ_create("1.2.3.4", "Alias", "Test alias Extension");
add_ext(x, nid, "Test comment alias");
@flavio
Copy link

flavio commented Feb 4, 2022

I'm interested in doing that too. I looked deeper, starting from d8f299fbb, OBJ_create is exported. This is done via openssl::nid::Nid::create.

Note well: this code isn't part of an official release yet - hence I've been building against the latest commit from the master branch

I've tried to put make use of it, but the code is still panicking at runtime.

The code looks like that:

const SIGSTORE_ISSUER_OID: &str = "1.3.6.1.4.1.57264.1.1";
let issuer = "hello world";

let sigstore_issuer_nid = openssl::nid::Nid::create(SIGSTORE_ISSUER_OID, "sigstore", "Sigstore OIDC issuer")?;
let sigstore_subject_issuer_extension = X509Extension::new_nid(
  None,
  Some(&x509v3_context),
  sigstore_issuer_nid,
  &subject_issuer,
)?;

At runtime I get this error:

Error: error:22097081:X509 V3 routines:do_ext_nconf:unknown extension:crypto/x509v3/v3_conf.c:82:

@tpambor (sorry for the direct mention, but given you are the one who exposed OBJ_create... ): am I forgetting something or is still something missing from the library itself?

If you are interested I can share the code of the whole demo app on a gist.

@flavio
Copy link

flavio commented Feb 4, 2022

I think I sorted it out, almost...

I had to specify the value given to X509Extension::new_nid in the proper way. You have to follow what is documented under the "ARBITRARY EXTENSIONS" docs.

const SIGSTORE_ISSUER_OID: &str = "1.3.6.1.4.1.57264.1.1";
let issuer = "hello world";

// This is what solves the previous error
let value = format!("ASN1:UTF8String:{}", subject_issuer);

let sigstore_issuer_nid = openssl::nid::Nid::create(SIGSTORE_ISSUER_OID, "sigstore", "Sigstore OIDC issuer")?;
let sigstore_subject_issuer_extension = X509Extension::new_nid(
  None,
  Some(&x509v3_context),
  sigstore_issuer_nid,
  &value,
)?;

Now the certificate is generated, however... when looking into that I get a "strange" output:

$ cargo run | openssl x509 -noout -text -in -
[...]
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            d4:c4:1d:07:83:29:1f:f7:97:c8:f1:2d:c0:dc:cf:0e:a5:c7:69
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: O = tests, CN = sigstore.test
        Validity
            Not Before: Feb  3 18:24:47 2022 GMT
            Not After : Feb  5 18:24:47 2022 GMT
        Subject: O = tests, CN = sigstore.test
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:89:49:4b:a3:52:06:62:f8:48:ff:ba:f7:9e:32:
                    79:51:e9:f9:14:fa:64:b8:e1:75:41:6e:49:04:19:
                    ac:d5:5b:15:b8:74:9c:a8:a7:6b:5d:71:ca:72:ee:
                    b7:b8:2e:81:53:99:bc:95:bc:bf:5d:82:4b:c9:da:
                    d1:09:8f:ef:5e
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                28:60:DE:28:1F:37:34:EB:6B:6F:07:0E:9B:4E:09:97:88:73:A1:70
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                Code Signing
            X509v3 Authority Key Identifier: 
                keyid:70:15:4F:33:7A:EE:CA:68:F8:86:D3:C2:BF:B8:A8:CC:50:66:55:8B

            X509v3 Subject Alternative Name: critical
                email:test@sigstore.dev
            1.3.6.1.4.1.57264.1.1: 
                ..https://sigstore.dev/oauth
    Signature Algorithm: ecdsa-with-SHA256
         30:46:02:21:00:8f:87:d0:36:10:cb:f4:3b:3d:2d:9b:64:ab:
         38:04:49:c4:7a:01:52:2e:2e:b8:9e:f2:74:3a:d1:78:00:0f:
         a1:02:21:00:93:f9:28:f6:24:84:23:02:48:43:63:04:de:07:
         cb:df:21:0c:e7:3c:64:b8:dc:30:35:ed:6b:4e:dc:e3:ab:97

The strange output are the .. the prefix the actual value https://sigstore.dev/oauth

@tpambor
Copy link
Contributor

tpambor commented Feb 4, 2022

@flavio I think you figured it out. I'm doing it similarly. .. is displayed because the custom extension has not been defined in openssl.cnf and therefore is not parsed by openssl. Openssl here displays the raw ASN.1 sequence. The first . stands for UTF8String (0c, https://obj-sys.com/asn1tutorial/node128.html), while the second . stands for the length of the string. Openssl just displays the ASCII representation.

@flavio
Copy link

flavio commented Feb 7, 2022

Thanks for the help @tpambor and for the explanation about the "mysterious" chars.

There's still something that drives me crazy... I'm trying to recreate something similar to this certificate:

-----BEGIN CERTIFICATE-----
MIIDRTCCAsygAwIBAgIUANUs+sqMabakcgjGJVHXWtImIOcwCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MjAxMjUxODA3MTRaFw0yMjAxMjUxODE3MTNaMBMxETAPBgNVBAoTCHNpZ3N0b3Jl
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmOwkohMNTIMKSHSNsjPnQJtFPSOO
4d422oKFm1EC1ffVwRZzidauBX3zpRWFVMHlyUWHS2Qqzgt8JEdwyHEcY6OCAeUw
ggHhMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAMBgNVHRMB
Af8EAjAAMB0GA1UdDgQWBBRfD6LV5vv5JNuoL/uEaF1SfmrqWTAfBgNVHSMEGDAW
gBRYwB5fkUWlZql6zJChkyLQKsXF+jBxBgNVHREEajBohmZodHRwczovL2dpdGh1
Yi5jb20va3ViZXdhcmRlbi9naXRodWItYWN0aW9ucy8uZ2l0aHViL3dvcmtmbG93
cy9yZXVzYWJsZS1yZWxlYXNlLXJ1c3QueW1sQHJlZnMvaGVhZHMvdjEwEgYKKwYB
BAGDvzABAgQEcHVzaDA0BgorBgEEAYO/MAEFBCZrdWJld2FyZGVuL2FsbG93ZWQt
ZnNncm91cHMtcHNwLXBvbGljeTA2BgorBgEEAYO/MAEDBChkZDNlZWM3M2RmNzU2
ZTlmZmQzOTQwMjMxYTE4ZGU4MDIyZDhkOGZiMDkGCisGAQQBg78wAQEEK2h0dHBz
Oi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHAYKKwYBBAGD
vzABBAQOUmVsZWFzZSBwb2xpY3kwHgYKKwYBBAGDvzABBgQQcmVmcy90YWdzL3Yw
LjEuMzAKBggqhkjOPQQDAwNnADBkAjAxyO2nqRLU6KBg83Fpa//TLruGz7wY7qT+
iLNh8GBZvmxIquRcnERXyqnFcwBT+xQCMHftLFa5UX/gTeOEak/AyzQwBcwhdcqF
WwHFs1oN2dNK+OoZlCcY7xvVfiKQ02jy0w==
-----END CERTIFICATE-----

This is created by a Go program, and it has all its arbitrary attributes without these symbols:

openssl x509 -noout -text -in demo-cert.pem
[snip]
            1.3.6.1.4.1.57264.1.1: 
                https://token.actions.githubusercontent.com
[snip]

I've used the der, der-parser crates to write a program that dumps low level information about the 2 certificates: the "oringal" one created by Go and the one I'm creating using the openssl crate.

This is what I get for the Go certificate:

BerObject {
  header: BerObjectHeader { class: Universal, structured: 1, tag: Sequence, len: Definite(57), raw_tag: Some([48]) },
  content: Sequence([
    BerObject {
      header: BerObjectHeader { class: Universal, structured: 0, tag: Oid, len: Definite(10), raw_tag: Some([6]) },
      content: OID(OID(1.3.6.1.4.1.57264.1.1))
    },
    BerObject {
      header: BerObjectHeader { class: Universal, structured: 0, tag: OctetString, len: Definite(43), raw_tag: Some([4]) },
      content: OctetString(
        [104, 116, 116, 112, 115, 58, 47, 47, 116, 111, 107, 101, 110, 46, 97, 99, 116, 105, 111, 110, 115, 46, 103, 105, 116, 104, 117, 98, 117, 115, 101, 114, 99, 111, 110, 116, 101, 110, 116, 46, 99, 111, 109])
    }
  ])
}

As you can see, the string is encoded using a OctectString.

I've changed my rust code, the one producing the certificate to do something like that:

            let sigstore_issuer_nid =
                openssl::nid::Nid::create(SIGSTORE_ISSUER_OID, "sigstore", "Sigstore OIDC issuer")?;

            let mut buffer = [0u8; 1000];
            let mut der_encoder = der::Encoder::new(&mut buffer);

            let data = der::asn1::OctetString::new(&subject_issuer.as_bytes())
                .map_err(|e| anyhow!("{:?}", e))?;
            der_encoder
                .encode(&data)
                .map_err(|e| anyhow!("Cannot encode subject_issuer to DER: {}", e))?;
            let encoded_string = der_encoder
                .finish()
                .map_err(|e| anyhow!("Cannot finish encoding subject_issuer to DER: {}", e))?;

            let hex_string: Vec<String> = encoded_string
                .iter()
                .map(|v| format!("{:02X}", v))
                .collect();
            let value = format!("DER:{}", hex_string.join(""));

            let sigstore_subject_issuer_extension = X509Extension::new_nid(
                None,
                Some(&x509v3_context),
                sigstore_issuer_nid,
                &value,
            )?;

Unfortunately the final cert still has the extra ASCII symbols:

openssl x509 -noout -text -in rust.pem 
[snip]
            1.3.6.1.4.1.57264.1.1: 
                .+https://token.actions.githubusercontent.com
[snip]

Looking closer at the der structure I get:

BerObject {
  header: BerObjectHeader { class: Universal, structured: 1, tag: Sequence, len: Definite(59), raw_tag: Some([48]) },
  content: Sequence([
    BerObject {
      header: BerObjectHeader { class: Universal, structured: 0, tag: Oid, len: Definite(10), raw_tag: Some([6]) },
      content: OID(OID(1.3.6.1.4.1.57264.1.1))
    },
    BerObject {
      header: BerObjectHeader { class: Universal, structured: 0, tag: OctetString, len: Definite(45), raw_tag: Some([4]) },
      content: OctetString(
        [4, 43, 104, 116, 116, 112, 115, 58, 47, 47, 116, 111, 107, 101, 110, 46, 97, 99, 116, 105, 111, 110, 115, 46, 103, 105, 116, 104, 117, 98, 117, 115, 101, 114, 99, 111, 110, 116, 101, 110, 116, 46, 99, 111, 109])
    }
  ])
}

They are basically the same, but it looks something is adding the extra 2 chars... 🤯

Why am I so obsessed by these 2 extra chars? Because when parsing the certificate using x509_parser I get them back!

const ISSUER_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .57264 .1 .1);

fn inspect_cert(name: &str) -> Result<()> {
    let cert_raw = fs::read(name)?;
    let (_, pem) = parse_x509_pem(&cert_raw)?;
    let cert = pem.parse_x509()?;

    let extensions = cert.tbs_certificate.extensions_map()?;
    for (oid, ext) in extensions {
        if oid == ISSUER_OID {
            println!("oid: {}", oid);
            println!("ext: {:?}", ext);

            let value = String::from_utf8(ext.value.to_vec());
            println!("value is: {:?}", value);
        }
    }

    Ok(())
}

This is the output I get:

Inspecting go.pem
oid: 1.3.6.1.4.1.57264.1.1
ext: X509Extension { oid: OID(1.3.6.1.4.1.57264.1.1), critical: false, value: [104, 116, 116, 112, 115, 58, 47, 47, 116, 111, 107, 101, 110, 46, 97, 99, 116, 105, 111, 110, 115, 46, 103, 105, 116, 104, 117, 98, 117, 115, 101, 114, 99, 111, 110, 116, 101, 110, 116, 46, 99, 111, 109], parsed_extension: UnsupportedExtension { oid: OID(1.3.6.1.4.1.57264.1.1) } }
value is: Ok("https://token.actions.githubusercontent.com")

Inspecting rust.pem
oid: 1.3.6.1.4.1.57264.1.1
ext: X509Extension { oid: OID(1.3.6.1.4.1.57264.1.1), critical: false, value: [4, 43, 104, 116, 116, 112, 115, 58, 47, 47, 116, 111, 107, 101, 110, 46, 97, 99, 116, 105, 111, 110, 115, 46, 103, 105, 116, 104, 117, 98, 117, 115, 101, 114, 99, 111, 110, 116, 101, 110, 116, 46, 99, 111, 109], parsed_extension: UnsupportedExtension { oid: OID(1.3.6.1.4.1.57264.1.1) } }
value is: Ok("\u{4}+https://token.actions.githubusercontent.com")

Sorry about the noise, I hope you didn't mind being my rubber duck debugging companion 😄

@tpambor
Copy link
Contributor

tpambor commented Feb 7, 2022

@flavio Seems you are right. I created a UTF8String with content "Something". Encoded that is:

0C 09 53 6F 6D 65 74 68 69 6E 67
0C 09 UTF8String, length 9, content: 53 6F 6D 65 74 68 69 6E 67

If I look at it in a hex editor I get:

04 0B 0C 09 53 6F 6D 65 74 68 69 6E 67
04 0B Octet string, length 12, content: 0C 09 53 6F 6D 65 74 68 69 6E 67

So it seems it is nested inside a octet string but I don't know exactly why. That happens here: https://github.com/openssl/openssl/blob/af16097febcd4fa31cd5fcd05ad09cf8b53659ea/crypto/x509/v3_conf.c#L258
It was added with openssl/openssl@388ff0b

akrantz01 added a commit to akrantz01/lers that referenced this issue Apr 4, 2023
Adds a solver for the
[`TLS-ALPN-01`](https://www.rfc-editor.org/rfc/rfc8737.html) challenge.

We needed to re-implement parts of
[`tokio-native-tls`](https://crates.io/crates/tokio-native-tls) and
[`native-tls`](https://crates.io/crates/native-tls) since they do not
provide enough control over the SSL acceptor in favor of cross-system
compatibility. Furthermore, we also needed to pull in
[`rcgen`](https://crates.io/crates/rcgen) since `openssl` does not
support [custom
extensions](sfackler/rust-openssl#1411) or
[marking them as
critical](sfackler/rust-openssl#1601).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants