open-policy-agent / opa

Open Policy Agent (OPA) is an open source, general-purpose policy engine.
https://www.openpolicyagent.org
Apache License 2.0
9.71k stars 1.35k forks source link

Decode SANS with crypto.x509.parse_certificate_request #6268

Open 3goats opened 1 year ago

3goats commented 1 year ago

Hi as per item 490 in the discussion area I need to properly decode the SAN values for an X.509 certificate request.

e.g.

output := crypto.x509.parse_certificate_request(input.csr)
y := base64.decode(output.Extensions[0].Value)

It appears the the SAN Extension does not get decoded during the parse and remains as a DER encoded, ASN.1 value. e.g. the output of the y value above is:

"y": "0!\ufffd\u001f\u0006\n+\u0006\u0001\u0004\u0001\ufffd7\u0014\u0002\u0003\ufffd\u0011\f\[u000f3goats@acme.com](mailto:u000f3goats@acme.com)"

Here's an example CSR that contains the SAN extension:

"-----BEGIN CERTIFICATE REQUEST-----\r\nMIICnDCCAYQCAQAwGjEYMBYGA1UEAwwPM2dvYXRzLmFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEF\r\nAAOCAQ8AMIIBCgKCAQEAuLTGpuFQyUDPZAqCw7sqO352GYG8AfkjP5eUCXjdWgmZqvnfGODmJ1Lp\r\n3xndPwfEdr+g3Bgg/ZEFyi67bePEo9+mzGJRSxerC/sMs/9vduNBwQrYAK1AvdlfHk9Lh5K86y6M\r\n+JgvFiZOYzI3mEpX1bMiOajk+pQtB+x66xfPE1mWcjY5TLBHvzhfyKXMRVayxUPb80FuWGsVXd+S\r\nc8jvunG4qf51ZF4omV/koc4z2RaY+s4eaB4z3zaH23FK5kW+APt5krBxM082kfsSDo3zMJ1hY0Op\r\nIqRagNYHlFLDeG1N5MGJJ/CBfq4QDudtVVetr5sEq2REBqCkAwVtytv2qQIDAQABoD0wOwYJKoZI\r\nhvcNAQkOMS4wLDAqBgNVHREEIzAhoB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29tMA0G\r\nCSqGSIb3DQEBCwUAA4IBAQB+YDsTOg2/+Jd3SMrbB3y5qzx17wYfIKec6j6wNYDv7grnFL3kNXDG\r\nSVU3UM1j07rfe/zb19ilSydcCqSM9cT466PxGvnVuBxXzlrZmVCl0agpidwKhF9JT6dH7F4+Lr+8\r\nt89uTGhAX2f0XnYgR0fhMMQRdZ32yfNCz5oWf7OIKJTFNgy1r69+RI13aY+W79f7PS9HU9FIfJyj\r\nuUDkDUb6Rp/7Jo8qknDkiiTI2cSHzQhOdUIQbasNy1ZeDmdpofjCW6+WQJvz8Xzj9NUSQEjoQbq0\r\nenV5YeJkS8ZDUyJ6baKdaNFVsoyG4aMqm5Ru0WLSAlb9/lMaZ7Ew8HVM2SDY\r\n-----END CERTIFICATE REQUEST-----"
charlieegan3 commented 1 year ago

Hi, thanks for opening the issue. I think there are two things here we need to work out:

Email SAN format

I'm not sure what's going on here and have created this Go program to demonstrate the issue: https://go.dev/play/p/AqUUuwPs2yd.

package main

import (
    "bytes"
    "crypto/ed25519"
    "crypto/rand"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/json"
    "encoding/pem"
    "fmt"
)

func main() {
    // parse and extract email from example CSR

    rawCSR := `-----BEGIN CERTIFICATE REQUEST-----
MIICnDCCAYQCAQAwGjEYMBYGA1UEAwwPM2dvYXRzLmFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAuLTGpuFQyUDPZAqCw7sqO352GYG8AfkjP5eUCXjdWgmZqvnfGODmJ1Lp
3xndPwfEdr+g3Bgg/ZEFyi67bePEo9+mzGJRSxerC/sMs/9vduNBwQrYAK1AvdlfHk9Lh5K86y6M
+JgvFiZOYzI3mEpX1bMiOajk+pQtB+x66xfPE1mWcjY5TLBHvzhfyKXMRVayxUPb80FuWGsVXd+S
c8jvunG4qf51ZF4omV/koc4z2RaY+s4eaB4z3zaH23FK5kW+APt5krBxM082kfsSDo3zMJ1hY0Op
IqRagNYHlFLDeG1N5MGJJ/CBfq4QDudtVVetr5sEq2REBqCkAwVtytv2qQIDAQABoD0wOwYJKoZI
hvcNAQkOMS4wLDAqBgNVHREEIzAhoB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29tMA0G
CSqGSIb3DQEBCwUAA4IBAQB+YDsTOg2/+Jd3SMrbB3y5qzx17wYfIKec6j6wNYDv7grnFL3kNXDG
SVU3UM1j07rfe/zb19ilSydcCqSM9cT466PxGvnVuBxXzlrZmVCl0agpidwKhF9JT6dH7F4+Lr+8
t89uTGhAX2f0XnYgR0fhMMQRdZ32yfNCz5oWf7OIKJTFNgy1r69+RI13aY+W79f7PS9HU9FIfJyj
uUDkDUb6Rp/7Jo8qknDkiiTI2cSHzQhOdUIQbasNy1ZeDmdpofjCW6+WQJvz8Xzj9NUSQEjoQbq0
enV5YeJkS8ZDUyJ6baKdaNFVsoyG4aMqm5Ru0WLSAlb9/lMaZ7Ew8HVM2SDY
-----END CERTIFICATE REQUEST-----`

    p, _ := pem.Decode([]byte(rawCSR))
    if p != nil && p.Type != "CERTIFICATE REQUEST" {
        panic("invalid PEM-encoded certificate signing request")
    }

    rawCSRParsed, err := x509.ParseCertificateRequest(p.Bytes)
    if err != nil {
        panic(err)
    }

    var rawCSRExtValues []asn1.RawValue
    _, err = asn1.Unmarshal(rawCSRParsed.Extensions[0].Value, &rawCSRExtValues)
    if err != nil {
        panic(err)
    }

    bs, err := json.MarshalIndent(rawCSRExtValues, "", "  ")
    if err != nil {
        panic(err)
    }

    fmt.Println("provided CSR email SAN value")
    fmt.Println(string(rawCSRExtValues[0].Bytes))
    fmt.Println(string(bs))

    // Create a fresh CR for comparison
    _, privateKey, err := ed25519.GenerateKey(rand.Reader)
    if err != nil {
        panic(err)
    }

    // 1 is nameTypeEmail
    derBytes, err := asn1.Marshal([]asn1.RawValue{asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte("3goats@acme.com")}})
    if err != nil {
        panic(err)
    }
    req := &x509.CertificateRequest{
        ExtraExtensions: []pkix.Extension{
            {
                Id:    []int{2, 5, 29, 17}, // oidExtensionSubjectAltName
                Value: derBytes,
            },
        },
    }
    newCSR, err := x509.CreateCertificateRequest(rand.Reader, req, privateKey)
    if err != nil {
        panic(err)
    }

    buf := bytes.NewBuffer([]byte{})
    err = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: newCSR})
    if err != nil {
        panic(err)
    }

    newCSRDecoded, _ := pem.Decode(buf.Bytes())
    newCSRParsed, err := x509.ParseCertificateRequest(newCSRDecoded.Bytes)
    if err != nil {
        panic(err)
    }

    var newCSRExtValues []asn1.RawValue
    _, err = asn1.Unmarshal(newCSRParsed.Extensions[0].Value, &newCSRExtValues)
    if err != nil {
        panic(err)
    }

    bs, err = json.MarshalIndent(newCSRExtValues, "", "  ")
    if err != nil {
        panic(err)
    }

    fmt.Println("\n---\nnew CSR email SAN value")
    fmt.Println(string(newCSRExtValues[0].Bytes))
    fmt.Println(string(bs))
}

I get this output:

provided CSR email SAN value

+�7�
    3goats@acme.com
[
  {
    "Class": 2,
    "Tag": 0,
    "IsCompound": true,
    "Bytes": "BgorBgEEAYI3FAIDoBEMDzNnb2F0c0BhY21lLmNvbQ==",
    "FullBytes": "oB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29t"
  }
]

---
new CSR email SAN value
3goats@acme.com
[
  {
    "Class": 2,
    "Tag": 1,
    "IsCompound": false,
    "Bytes": "M2dvYXRzQGFjbWUuY29t",
    "FullBytes": "gQ8zZ29hdHNAYWNtZS5jb20="
  }
]

It appears some additional bytes are presenting in the value in the example CSR provided. I'm not sure where these are coming from and they're not present when I recreate a comparable CSR from scratch in Go with the same email.

@3goats, can you share any details about where this CSR is coming from and why we might be seeing this?

what OPA could to do?

If we added a built-in wrapping asn1.Unmarshal which unmarshalled to a []asn1.RawValue that might be suitable?

3goats commented 1 year ago

"If we added a built-in wrapping asn1.Unmarshal which unmarshalled to a []asn1.RawValue that might be suitable?"

Yes I think that would do it.

I used this to create the CSR: https://keystore-explorer.org

Here's another CSR which I ran through openssl. The othername: UPN::3goats@acme.com looks OK there.

openssl req -text -noout -verify -in ~/3goats.acme.com.csr

Certificate request self-signature verify OK
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: CN = 3goats.acme.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ab:46:bb:b5:e4:2d:3a:a9:13:73:eb:b0:58:46:
                    88:5c:0a:69:96:a9:eb:49:46:2f:e0:91:a3:95:fa:
                    e8:0c:34:01:44:03:78:69:e6:19:66:6d:e6:2a:ed:
                    fd:a6:7d:42:f6:4d:31:d7:70:cd:52:4b:a4:b7:e2:
                    05:13:95:5e:c6:d9:6e:cd:93:e9:1b:52:67:52:5d:
                    57:a8:51:d2:97:81:2b:35:c3:3b:73:a9:a6:a4:30:
                    aa:57:02:d2:42:cc:d8:3b:d3:fe:30:3e:dc:69:13:
                    14:0a:c7:5a:9d:ea:3b:cb:6b:ab:c7:f8:62:3f:d0:
                    2a:71:27:a5:30:4a:41:be:10:98:25:d1:10:60:a5:
                    34:64:4a:d1:c5:08:3b:eb:4e:6e:bb:b4:a0:00:4c:
                    0d:36:9e:26:7b:b1:cf:f0:a7:90:9d:87:f0:69:9d:
                    19:5f:e8:4c:ac:09:70:d2:4d:a6:74:a6:ac:3b:50:
                    4a:64:ce:b8:93:ca:96:41:95:d7:d4:17:81:8e:79:
                    99:39:98:42:79:f7:cd:a5:35:b0:55:16:f2:7a:96:
                    6e:8d:40:79:87:57:be:42:e7:ae:f3:95:92:d1:9b:
                    5a:23:99:c6:36:27:50:b6:20:6c:e6:39:8a:e4:f7:
                    9b:20:76:db:a1:e5:31:3f:fb:89:36:64:4b:1a:c3:
                    65:07
                Exponent: 65537 (0x10001)
        Attributes:
            Requested Extensions:
                X509v3 Subject Alternative Name: 
                    othername: UPN::3goats@acme.com
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        a5:7c:0e:08:29:4a:6c:0e:25:8b:2c:e9:3b:ba:08:cb:e6:5e:
        76:04:3f:bd:be:4d:78:f5:5f:b1:58:d5:9d:b3:69:f7:0a:57:
        83:4c:c9:88:1b:96:7c:e4:04:3e:84:c7:92:74:e7:21:a8:76:
        94:4a:a5:24:ac:a9:01:55:42:05:33:5e:e3:54:80:d9:f9:4b:
        4f:ea:6e:5e:b4:e8:4e:a8:e8:02:e4:e3:04:10:f3:2a:72:9f:
        ba:34:51:af:69:57:27:f7:b3:8b:b1:5f:26:c9:79:12:35:f8:
        d6:81:44:b1:8e:bd:0c:5a:97:f5:d8:ea:8c:d1:76:2d:57:60:
        5f:bc:ab:5a:7c:28:63:fd:00:7d:13:c4:bb:36:43:83:3e:62:
        a1:62:e9:cf:6e:1d:3c:70:9e:4f:10:de:e4:22:e3:35:e5:2a:
        0c:9a:b9:5c:1b:20:8c:4d:8b:ae:e3:08:52:c0:d1:40:05:a1:
        5e:98:2e:6e:9b:f5:33:d9:01:fb:4a:12:d5:82:26:a3:df:d7:
        81:76:02:5b:07:2d:80:11:2f:a2:0f:9b:0d:78:29:b1:14:b6:
        2f:2f:50:11:1f:de:97:ca:14:ce:d5:c8:f6:d2:72:4c:34:0e:
        94:b2:82:d6:65:44:c6:30:e1:11:a3:94:f9:b5:f5:f4:58:f4:
        cc:a4:33:69

Here's the CSR.

-----BEGIN CERTIFICATE REQUEST-----
MIICnDCCAYQCAQAwGjEYMBYGA1UEAwwPM2dvYXRzLmFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAsMWNfjdYm8jr57nMrs3ubdS20GDTcLzyu2KQqhGFCMY7COaVCP9ndZVv
nFv7q2LRB8P5MA9ROYNAXqgrF9CatWiaL1WaB3A5VICj3M9iQnaPw7XpZJW+GvZTltDOWhW0kPSW
3aQidsVocPGol2Co1qVrD3GXu610+EgDkSkyEI2/rMJPtjYf9OSuZoHeZn8xzny6+nlFQKVhHQ16
3blPkkrKMe6KQApGs49x9HvQAUT7UfMIb4btQMW/6+wQfWC/t0y0IsRU0fLiOr6+r4jYKAhewSEF
Pii4y4ds9GK3ZziaXPxPlDonyzezePJUiTRHJY/HEHnkmo+VX3rpzVdTFwIDAQABoD0wOwYJKoZI
hvcNAQkOMS4wLDAqBgNVHREEIzAhoB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29tMA0G
CSqGSIb3DQEBCwUAA4IBAQABLr+BhRi4/Kb86kt2aO7J3FxdlPaEG6aUCxcbXkW5sGzxcmT2BSJQ
k2zDDu6t4paFV8sdWspb3IFdnF4loG/PKOaBOjXcfyaBk5mXWIcb7N/QhKHtgc79yPf3ywW/+FUy
97aNCtcyGuz54GRgGI/VValnQBjqoZ7cqPdb+TmSu8Zmn3hfF5Evs9AKWLaHBkPcb8//qQJFlqc3
Vr7q+PwwKejeH83BzE0jKW3l95no6H0M3Ng5trzS7aooD/24xe6lzRc1NnHJ3/mXVk9BvPu1H6yP
KkR5sV2iISL9klJn+YmoLOcr92mg/WfSE3bvaDYnjEGiunSNh+nZlBcRZVUA
-----END CERTIFICATE REQUEST-----
charlieegan3 commented 1 year ago

Yeah that's interesting. 🤔 I'd be keen to get to the bottom of how to decode those extra bytes in Go before we decide on an implementation.

charlieegan3 commented 1 year ago

After asking around and looking into this a little more, I think it's going to be hard to build something that's generic here.

It looks like your example CR features the Microsoft UPN Extension, this would custom unmarshalling (see example here). In theory, the format of different extensions can be quite fluid and I'm not sure a good generic implementation exists.

Supporting the unmarshalling of Microsoft UPN formatted x509 attributes is something we could consider, though I don't know of it coming up before.

Based on the discussion here: https://github.com/orgs/open-policy-agent/discussions/493#discussioncomment-7183878 it looks like substring matching might be sufficient for your use case? If so, I think we can close this.

3goats commented 1 year ago

Sorry for the delay.

OK could we maybe just unmarshall the UPN in this case then. I'm going to need this for a specific requirement. Maybe for the others we will need to cross that bridge at the time.

SpectralHiss commented 1 year ago

I was gonna say exactly that, the code: \u001f\u0006\n+\u0006\u0001\u0004\u0001\ufffd7\u0014\u0002\u0003 corresponds to the UPN OID. otherName offers a "generic" sequence escape latch for possible sans:

OtherName ::= SEQUENCE {
    type-id    OBJECT IDENTIFIER,
    value      [0] EXPLICIT ANY DEFINED BY type-id }

In our case we have a sequence containing 1 element with the type-id UPN and the value of 3goats..

(Also Hi Charlie :wave: :) )

charlieegan3 commented 1 year ago

Ok gotcha, I'd not realised that there was an escape hatch for otherName in the spec that this was just one example of.

I guess then, this issue is about supporting unmarshalling of data in this structure.

   SubjectAltName ::= GeneralNames

   GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

   GeneralName ::= CHOICE {
        otherName                       [0]     OtherName,
        rfc822Name                      [1]     IA5String,
        dNSName                         [2]     IA5String,
        x400Address                     [3]     ORAddress,
        directoryName                   [4]     Name,
        ediPartyName                    [5]     EDIPartyName,
        uniformResourceIdentifier       [6]     IA5String,
        iPAddress                       [7]     OCTET STRING,
        registeredID                    [8]     OBJECT IDENTIFIER }

   OtherName ::= SEQUENCE {
        type-id    OBJECT IDENTIFIER,
        value      [0] EXPLICIT ANY DEFINED BY type-id }

   EDIPartyName ::= SEQUENCE {
        nameAssigner            [0]     DirectoryString OPTIONAL,
        partyName               [1]     DirectoryString }

From https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6

3goats commented 1 year ago

Hey - any updates on this one ? Is this something that can be implemented ?

charlieegan3 commented 1 year ago

I've had another look into a proof of concept implementation:

package main

import (
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/json"
    "encoding/pem"
    "fmt"
)

const oidExtensionSubjectAltName = "2.5.29.17"

type OtherName struct {
    ID    asn1.ObjectIdentifier
    Value string `asn1:"utf8,explicit,tag:0"`
}

func unmarshalExts(exts []pkix.Extension) (map[string]interface{}, error) {
    m := make(map[string]interface{})
    for _, e := range exts {
        var v []asn1.RawValue
        if _, err := asn1.Unmarshal(e.Value, &v); err != nil {
            return nil, err
        }

        switch e.Id.String() {
        case oidExtensionSubjectAltName:
            var gns []interface{}
            for _, raw := range v {
                switch raw.Tag {
                case 0:
                    var on OtherName
                    if _, err := asn1.UnmarshalWithParams(raw.FullBytes, &on, "tag:0"); err != nil {
                        return nil, err
                    }
                    gns = append(gns, on)
# TODO handle dns names, emails etc.
                }
            }

            m[e.Id.String()] = gns
        default:
            m[e.Id.String()] = v
        }

    }
    return m, nil
}

func main() {
    rawCSR := `-----BEGIN CERTIFICATE REQUEST-----
MIICnDCCAYQCAQAwGjEYMBYGA1UEAwwPM2dvYXRzLmFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAuLTGpuFQyUDPZAqCw7sqO352GYG8AfkjP5eUCXjdWgmZqvnfGODmJ1Lp
3xndPwfEdr+g3Bgg/ZEFyi67bePEo9+mzGJRSxerC/sMs/9vduNBwQrYAK1AvdlfHk9Lh5K86y6M
+JgvFiZOYzI3mEpX1bMiOajk+pQtB+x66xfPE1mWcjY5TLBHvzhfyKXMRVayxUPb80FuWGsVXd+S
c8jvunG4qf51ZF4omV/koc4z2RaY+s4eaB4z3zaH23FK5kW+APt5krBxM082kfsSDo3zMJ1hY0Op
IqRagNYHlFLDeG1N5MGJJ/CBfq4QDudtVVetr5sEq2REBqCkAwVtytv2qQIDAQABoD0wOwYJKoZI
hvcNAQkOMS4wLDAqBgNVHREEIzAhoB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29tMA0G
CSqGSIb3DQEBCwUAA4IBAQB+YDsTOg2/+Jd3SMrbB3y5qzx17wYfIKec6j6wNYDv7grnFL3kNXDG
SVU3UM1j07rfe/zb19ilSydcCqSM9cT466PxGvnVuBxXzlrZmVCl0agpidwKhF9JT6dH7F4+Lr+8
t89uTGhAX2f0XnYgR0fhMMQRdZ32yfNCz5oWf7OIKJTFNgy1r69+RI13aY+W79f7PS9HU9FIfJyj
uUDkDUb6Rp/7Jo8qknDkiiTI2cSHzQhOdUIQbasNy1ZeDmdpofjCW6+WQJvz8Xzj9NUSQEjoQbq0
enV5YeJkS8ZDUyJ6baKdaNFVsoyG4aMqm5Ru0WLSAlb9/lMaZ7Ew8HVM2SDY
-----END CERTIFICATE REQUEST-----`

    p, _ := pem.Decode([]byte(rawCSR))
    if p != nil && p.Type != "CERTIFICATE REQUEST" {
        panic("invalid PEM-encoded certificate signing request")
    }

    rawCSRParsed, err := x509.ParseCertificateRequest(p.Bytes)
    if err != nil {
        panic(err)
    }

    var rawCSRExtValues []asn1.RawValue
    _, err = asn1.Unmarshal(rawCSRParsed.Extensions[0].Value, &rawCSRExtValues)
    if err != nil {
        panic(err)
    }

    out, err := unmarshalExts(rawCSRParsed.Extensions)
    if err != nil {
        panic(err)
    }

    outBS, err := json.MarshalIndent(out, "", "  ")
    if err != nil {
        panic(err)
    }

    fmt.Println(string(outBS))
}

https://github.com/sigstore/sigstore/blob/main/pkg/cryptoutils/sans.go provides some pointers.

As far as exposing in Rego goes... If we were to support this functionality, I think a built-in function like crypto.x509.parse_extensions would be a good name.

I'm not convinced that we need to implement all the Standard Extensions from the spec. Parsing SAN extensions seems like a good start. We'd want to make sure that we supported ipaddress, dnsnames, uris and emails in addition to other names (that's a TODO in the above code). I'm not sure if we'd also want to support the other types for GeneralName (registeredID, ediPartyName, directoryName etc as listed at the end of https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6). It seems like it'd be good to go for completeness of SAN unmarshalling so the builtin behaviour would be static for SANs at least into the future.

3goats commented 1 year ago

I would agree that this seems like a good approach. 

charlieegan3 commented 1 year ago

👍 Ok, I'll try and get something together this week.

3goats commented 1 year ago

Thanks Charlie.

charlieegan3 commented 1 year ago

Hey, short update on this. I have taken a look into unmarhsalling some 'blessed' otherName values with mixed results. I was using the UPN example in addition to a kerberos principal name and sigstore cert.

As I've been getting my head around the ASN.1 encoding, I've been wondering if something that did a generic ASN.1 to JSON might be a better option.

[0] (2 elem)
  OBJECT IDENTIFIER 1.3.6.1.4.1.311.20.2.3
  [0] (1 elem)
    UTF8String 3goats@acme.com

Might look something like:

[
  {
    "oid": "1.3.6.1.4.1.311.20.2.3"
  },
  [
    {
      "string": "3goats@acme.com"
    }
  ]
]

Interested to know what you think of that approach, and I can circle back on that next week sometime.

inteon commented 1 year ago

I wonder if we could maybe create a seperate go library that handles encoding & decoding of all these special cases that are not handled by the go x509 library.

It would be very useful for cert-manager too.

charlieegan3 commented 1 year ago

Hey Tim, sorry for being a little quiet here this week. I've not been able to dig into this again. I think it'd be great if a library existed outwith OPA to process common patterns of ASN.1 data found in x509 documents. It'd make this feature much easier to implement and hopefully make things easier to standardise too.

3goats commented 1 year ago

Assume there's nothing in here that might help:

https://pkg.go.dev/golang.org/x/crypto/cryptobyte

https://go.googlesource.com/crypto/+/master/cryptobyte/asn1.go

charlieegan3 commented 1 year ago

Maybe... But I'm not sure how to hold it for the data we have here. Here's an example with your CSR and a Kerberos cert.

It fails at seq.ReadASN1ObjectIdentifier, I'm not sure what the correct use of the library is.

I think that even if we can read the OID out though, we're still stuck at the value [0] EXPLICIT ANY DEFINED BY type-id part of the spec where the data can be anything.

package main

import (
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/base64"
    "encoding/json"
    "encoding/pem"
    "fmt"

    "golang.org/x/crypto/cryptobyte"
    cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
)

const oidExtensionSubjectAltName = "2.5.29.17"

func unmarshalExts(exts []pkix.Extension) (map[string]interface{}, error) {
    m := make(map[string]interface{})
    for _, e := range exts {
        // ignore other exts
        if e.Id.String() != oidExtensionSubjectAltName {
            continue
        }

        input := cryptobyte.String(e.Value)

        var seq cryptobyte.String

        var ok bool
        ok = input.ReadASN1(&seq, cbasn1.SEQUENCE)
        if !ok {
            return nil, fmt.Errorf("expected seq")
        }
        fmt.Println(base64.StdEncoding.EncodeToString(seq))

        var oid asn1.ObjectIdentifier

        ok = seq.ReadASN1ObjectIdentifier(&oid)
        if !ok {
            return nil, fmt.Errorf("expected object identifier")
        }

        fmt.Println(oid.String())
    }
    return m, nil
}

func main() {

    csrUPN := `-----BEGIN CERTIFICATE REQUEST-----
MIICnDCCAYQCAQAwGjEYMBYGA1UEAwwPM2dvYXRzLmFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAsMWNfjdYm8jr57nMrs3ubdS20GDTcLzyu2KQqhGFCMY7COaVCP9ndZVv
nFv7q2LRB8P5MA9ROYNAXqgrF9CatWiaL1WaB3A5VICj3M9iQnaPw7XpZJW+GvZTltDOWhW0kPSW
3aQidsVocPGol2Co1qVrD3GXu610+EgDkSkyEI2/rMJPtjYf9OSuZoHeZn8xzny6+nlFQKVhHQ16
3blPkkrKMe6KQApGs49x9HvQAUT7UfMIb4btQMW/6+wQfWC/t0y0IsRU0fLiOr6+r4jYKAhewSEF
Pii4y4ds9GK3ZziaXPxPlDonyzezePJUiTRHJY/HEHnkmo+VX3rpzVdTFwIDAQABoD0wOwYJKoZI
hvcNAQkOMS4wLDAqBgNVHREEIzAhoB8GCisGAQQBgjcUAgOgEQwPM2dvYXRzQGFjbWUuY29tMA0G
CSqGSIb3DQEBCwUAA4IBAQABLr+BhRi4/Kb86kt2aO7J3FxdlPaEG6aUCxcbXkW5sGzxcmT2BSJQ
k2zDDu6t4paFV8sdWspb3IFdnF4loG/PKOaBOjXcfyaBk5mXWIcb7N/QhKHtgc79yPf3ywW/+FUy
97aNCtcyGuz54GRgGI/VValnQBjqoZ7cqPdb+TmSu8Zmn3hfF5Evs9AKWLaHBkPcb8//qQJFlqc3
Vr7q+PwwKejeH83BzE0jKW3l95no6H0M3Ng5trzS7aooD/24xe6lzRc1NnHJ3/mXVk9BvPu1H6yP
KkR5sV2iISL9klJn+YmoLOcr92mg/WfSE3bvaDYnjEGiunSNh+nZlBcRZVUA
-----END CERTIFICATE REQUEST-----`

    certKerberos := `-----BEGIN CERTIFICATE-----
MIID2zCCAsOgAwIBAgIUKGdEqu7o6HfNYvNzRMqA5MFvuK4wDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzEwMzExMDExMzJaFw0yNDEw
MzAxMDExMzJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCjuph5kaTvvd2xJ0HYFWrxjcLyISQCkucqIVP1YLTB
vvK+SwrXjGAOyPYUnL8NTTrIPGUuuMik8xKdYS2Fbn57Pse8TateYIB7y3tiPi1O
KEkEB16wam+HpqG8U273lAl8C2chwEnR7MnaYrOmiDK6j8uUgaeEDa7lAth05xNt
bkknPzT6xy30PC4wvhg55RsRdAJON1CVEKa/DzIHpgKEuSnIBV75NavIq9NF6MYd
RquvY6bPXRK0Yy/A/I4qwrnSKTW2aPJewRmXWKQnps+ohS9+ZCTme3+2cjJwL6dq
91qVZbPBgrU5v+CXD9+VteYFNyxYPrqR22hjI7taeKGnAgMBAAGjgcIwgb8wCQYD
VR0TBAIwADALBgNVHQ8EBAMCA6gwEgYDVR0lBAswCQYHKwYBBQIDBDAdBgNVHQ4E
FgQUbL4ZtZgpxz/nZDFv0d1Qhot3GFQwHwYDVR0jBBgwFoAUPRnKJ8PE+qJx95jJ
x7px6H6A53AwCQYDVR0SBAIwADBGBgNVHREEPzA9oDsGBisGAQUCAqAxMC+gEBsO
WU9VUl9SRUFMTU5BTUWhGzAZoAMCAQGhEjAQGw5ZT1VSX1BSSU5DTkFNRTANBgkq
hkiG9w0BAQsFAAOCAQEAU9Xlhsh8tp8psdyeQj3YcFgR/4dpy+TmIUToP+deukUQ
cpzev6e+tMtBwWwVJFuY3d5SVQBhrMF1x4/CmusCA6JuDrYKaCJGPuURvSaZ/CNb
fWuE/tdh1DxR20x4JruTiDpy3tVswAnOWKv6TWCqmdo9HydnLVx+7nXcbyzbZ8lX
U8GrBNFMcOI3rpYTeQWjzSbr2gGeM59CVlPqgLbG2WcN6bBSJDfiPk6rPGthzfph
jsDo7Ui1glzZOaHat9f17nMxpgTM8l+oqexvcUnZ+Cfr+FBRWkRNLsxBdOOPoBqY
wWy44hfcegrvch51oNMscwQ5NCJRGYI6q3T9yexVug==
-----END CERTIFICATE-----`

    allCerts := []struct {
        isCSR bool
        data  string
    }{
        {
            isCSR: true,
            data:  csrUPN,
        },
        {
            isCSR: false,
            data:  certKerberos,
        },
    }

    for _, cert := range allCerts {
        p, _ := pem.Decode([]byte(cert.data))

        var exts []pkix.Extension

        if !cert.isCSR {
            certParsed, err := x509.ParseCertificate(p.Bytes)
            if err != nil {
                panic(err)
            }

            exts = certParsed.Extensions
        } else {
            csrParsed, err := x509.ParseCertificateRequest(p.Bytes)
            if err != nil {
                panic(err)
            }

            exts = csrParsed.Extensions

        }

        out, err := unmarshalExts(exts)
        if err != nil {
            panic(err)
        }

        outBS, err := json.MarshalIndent(out, "", "  ")
        if err != nil {
            panic(err)
        }

        fmt.Println(string(outBS))
        fmt.Println("-----------------------")
    }
}
3goats commented 1 year ago

Wondering if this helps with this issue.

https://go.dev/play/p/DAzfVZ9HnT0

stale[bot] commented 11 months ago

This issue has been automatically marked as inactive because it has not had any activity in the last 30 days. Although currently inactive, the issue could still be considered and actively worked on in the future. More details about the use-case this issue attempts to address, the value provided by completing it or possible solutions to resolve it would help to prioritize the issue.