sfackler / rust-openssl

OpenSSL bindings for Rust
1.35k stars 728 forks source link

Custom X509 extensions #1411

Open ipetr0v opened 3 years ago

ipetr0v commented 3 years ago

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 commented 2 years ago

I'm interested in doing that too. I looked deeper, starting from https://github.com/sfackler/rust-openssl/commit/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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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... :exploding_head:

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 :smile:

tpambor commented 2 years ago

@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 https://github.com/openssl/openssl/commit/388ff0b076430b4fbcf5cf30575a304def28bf2d

e-cloud commented 1 month ago

@flavio I also experienced same problem today. The funny thing is, asking chatGPT output the same issue.

see https://github.com/openssl/openssl/issues/24415.

Accidentally, I made some modification, and it works now.

I tried to use the DER with ascii encoded value for hello world.

1.2.3.4.5 = DER:68656C6C6F20776F726C64

however, I don't know what's the correct format for ASN1.

BHawleyWall commented 6 days ago

Necro-ing this since it is still open 2.5 yrs later - and I hope this helps future seekers like myself.

This appears to occur due to an ASN.1 format default (IMPLICIT/EXPLICIT tagging):

When the IMPLICIT or EXPLICIT keyword is not present, the default is EXPLICIT, unless the module sets a different default at the top with “EXPLICIT TAGS,” “IMPLICIT TAGS,” or “AUTOMATIC TAGS.” For instance, RFC 5280 defines two modules, one where EXPLICIT tags are the default, and a second one that imports the first, and has IMPLICIT tags as the default. Implicit encoding uses fewer bytes than explicit encoding.

( ref for quote )

It seems the openssl code isn't bothering to set this keyword for a generic extension, and by ASN.1 rules that defaults most often to EXPLICIT, wrapping the OctetString in an additional copy of the BerObjectHeader's two bytes.