diff --git a/revocation/revocation.go b/revocation/revocation.go new file mode 100644 index 0000000..fcab598 --- /dev/null +++ b/revocation/revocation.go @@ -0,0 +1,55 @@ +package revocation + +import ( + "encoding/asn1" + "crypto/x509" +) + +// InfoArchival is the pkcs7 container containing the revocation information for +// all embedded certificates. +// +// Currently the internal structure is exposed but I don't like to expose the +// asn1.RawValue objects. We can probably make them private and expose the +// information with functions. +type InfoArchival struct { + CRL CRL `asn1:"tag:0,optional,explicit"` + OCSP OCSP `asn1:"tag:1,optional,explicit"` + Other Other `asn1:"tag:2,optional,explicit"` +} + +// AddCRL is used to embed an CRL to revocation.InfoArchival object. You directly +// pass the bytes of a downloaded CRL to this function. +func (r *InfoArchival) AddCRL(b []byte) error { + r.CRL = append(r.CRL, asn1.RawValue{FullBytes: b}) + return nil +} + +// AddOCSP is used to embed the raw bytes of an OCSP response. +func (r *InfoArchival) AddOCSP(b []byte) error { + r.OCSP = append(r.OCSP, asn1.RawValue{FullBytes: b}) + return nil +} + +// IsRevoked checks if there is a status inclded for the certificate and returns +// true if the certificate is marked as revoked. +// +// TODO: We should report if there is no CRL or OCSP response embeded for this certificate +// TODO: Information about the revocation (time, reason, etc) must be extractable +func (r *InfoArchival) IsRevoked(c *x509.Certificate) bool { + // check the crl and ocsp to see if this certificate is revoked + return true +} + +// CRL contains the raw bytes of a pkix.CertificateList and can be parsed with +// x509.PParseCRL. +type CRL []asn1.RawValue + +// OCSP contains the raw bytes of an OCSP response and can be parsed with +// x/crypto/ocsp.ParseResponse +type OCSP []asn1.RawValue + +// ANS.1 Object OtherRevInfo +type Other struct { + Type asn1.ObjectIdentifier + Value []byte +} diff --git a/sign/revocation.go b/sign/revocation.go new file mode 100644 index 0000000..5796e62 --- /dev/null +++ b/sign/revocation.go @@ -0,0 +1,88 @@ +package sign + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "bitbucket.org/digitorus/pdfsign/revocation" + + "golang.org/x/crypto/ocsp" +) + +func embedOCSPRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + req, err := ocsp.CreateRequest(cert, issuer, nil) + if err != nil { + return err + } + + ocspUrl := fmt.Sprintf("%s/%s", strings.TrimRight(cert.OCSPServer[0], "/"), + base64.StdEncoding.EncodeToString(req)) + resp, err := http.Get(ocspUrl) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // check if we got a valid OCSP response + _, err = ocsp.ParseResponseForCert(body, cert, issuer) + if err != nil { + return err + } + + i.AddOCSP(body) + return nil +} + +// embedCRLRevocationStatus requires an issuer as it needs to implement the +// the interface, a nil argment might be given if the issuer is not known. +func embedCRLRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + resp, err := http.Get(cert.CRLDistributionPoints[0]) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // TODO: verify crl and certificate before embedding + i.AddCRL(body) + return nil +} + +func embedRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + // For each certificate a revoction status needs to be included, this can be done + // by embedding a CRL or OCSP response. In most cases an OCSP response is smaller + // to embed in the document but and empty CRL (often seen of dediced high volume + // hirachies) can be smaller. + // + // There have been some reports that the usage of a CRL would result in a better + // compatibilty. + // + // TODO: Find and embed link about compatibilty + // TODO: Implement revocation status caching (required for higher volume signing) + + // using an OCSP server + if len(cert.OCSPServer) > 0 { + embedOCSPRevocationStatus(cert, issuer, i) + return nil + } + + // using a crl + if len(cert.CRLDistributionPoints) > 0 { + embedCRLRevocationStatus(cert, issuer, i) + return nil + } + + return errors.New("certificate contains no information to check status") +} diff --git a/sign/revocation_test.go b/sign/revocation_test.go new file mode 100644 index 0000000..34b79b0 --- /dev/null +++ b/sign/revocation_test.go @@ -0,0 +1,119 @@ +package sign + +import ( + "crypto/x509" + "encoding/pem" + "testing" + + "bitbucket.org/digitorus/pdfsign/revocation" +) + +const certPem = `-----BEGIN CERTIFICATE----- +MIIGKDCCBRCgAwIBAgIMW8J4m7huCPO5f+wbMA0GCSqGSIb3DQEBCwUAMEsxCzAJ +BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSEwHwYDVQQDExhH +bG9iYWxTaWduIENBIDIgZm9yIEFBVEwwHhcNMTcwNzA2MDk0MDUyWhcNMjAwNzA2 +MDk0MDUyWjCBpzELMAkGA1UEBhMCR0IxDTALBgNVBAgTBEtlbnQxEjAQBgNVBAcT +CU1haWRzdG9uZTEfMB0GA1UEChMWR01PIEdsb2JhbFNpZ24gTGltaXRlZDEfMB0G +A1UEAxMWUGF1bCBWYW4gQnJvdXdlcnNoYXZlbjEzMDEGCSqGSIb3DQEJARYkcGF1 +bC52YW5icm91d2Vyc2hhdmVuQGdsb2JhbHNpZ24uY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAr5jbAIZDjkWngxlwJqneE9VEDTvmMIGwvgy71g5j +k+igHxB6tTfaqGD87oIm2wcrlZHpPJG9n2Rh9FhFmvx8ZXiceNI9Ks5Ho5iYNFUk +y2JuVfFPxtp6amqpLzM5HZUePgu1Gdy1Zn1PUajii7paFPuhdemcA9DdTAQ1GsDv +C9MZ2D5sKM0hLCRePCzJ3TeQmHefFrC0XQ7u2i7LDD990URiFz7WNq2tSDJwBe/6 +1tMewpekmtE5X43PqzgcyGBsDJqAKcthsLQnhqrIryuwE2bEhP/FxQrHkT+f/OkK +vwAjB9gYgJRNoMFUwXLW789JsOkbCqepoWsg1PnWrAbAMQIDAQABo4ICrTCCAqkw +DgYDVR0PAQH/BAQDAgeAMIGIBggrBgEFBQcBAQR8MHowQQYIKwYBBQUHMAKGNWh0 +dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzYWF0bDJzaGEyZzIu +Y3J0MDUGCCsGAQUFBzABhilodHRwOi8vb2NzcDIuZ2xvYmFsc2lnbi5jb20vZ3Nh +YXRsMnNoYTJnMjCB2wYDVR0gBIHTMIHQMIHNBgsrBgEEAaAyASgeAjCBvTCBhgYI +KwYBBQUHAgIwegx4VGhpcyBjZXJ0aWZpY2F0ZSBoYXMgYmVlbiBpc3N1ZWQgaW4g +YWNjb3JkYW5jZSB3aXRoIHRoZSBHbG9iYWxTaWduIENQUyBsb2NhdGVkIGF0IGh0 +dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMDIGCCsGAQUFBwIB +FiZodHRwczovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzAJBgNVHRME +AjAAMD8GA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20v +Z3MvZ3NhYXRsMnNoYTJnMi5jcmwwLwYDVR0RBCgwJoEkcGF1bC52YW5icm91d2Vy +c2hhdmVuQGdsb2JhbHNpZ24uY29tMFwGCiqGSIb3LwEBCQEETjBMAgEBhkRodHRw +Oi8vYWF0bC10aW1lc3RhbXAuZ2xvYmFsc2lnbi5jb20vdHNhL2FvaGZld2F0MjM4 +OTUzNWZuYXNnbmxnNW0yMwEBADATBgNVHSUEDDAKBggrBgEFBQcDBDAdBgNVHQ4E +FgQUtimjVds00OgBZ747tqkKbZJeCCowHwYDVR0jBBgwFoAUxRNOzofGiRsj6EDj +dTKbA3A67+8wDQYJKoZIhvcNAQELBQADggEBACBUFVwUKVpOWt1eLf7lKKPfVhEL +9QrkAkV/UZPMsDwBDIJhphqjCqfbJVTgybm79gUCJiwbarCYOHRgFAdNTPEvEcT0 ++XwR6WZcDdfQAtaHfO6X9ExgJv93txoFVcpYLY1hR3o6QdP4VQSDhRTv3bM1j/WC +mcCoiIQz28Y8L+8rRx5J7JAgYpupoU/8sCpidBMhYAGF5p8Z8p0LbqvZndRHaVqp +yXQ0kYj1n45it5FXsKECWZKTx0v4IBySJY3RGpF+5cpPUYulJfINBg7nj7aQG/Uv +qtyxnVAG3W4pTHWd/0Gyc3lrgRtyZy+b9DaxHZ/N6HHNgnRHB4PUkektpX4= +-----END CERTIFICATE-----` + +const issuerPem = `-----BEGIN CERTIFICATE----- +MIIEpjCCA46gAwIBAgIORea3r9ymcb22XRTz2sAwDQYJKoZIhvcNAQELBQAwVzEL +MAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExLTArBgNVBAMT +JEdsb2JhbFNpZ24gQ0EgZm9yIEFBVEwgLSBTSEEyNTYgLSBHMjAeFw0xNDEyMTAw +MDAwMDBaFw0yNDEyMTAwMDAwMDBaMEsxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBH +bG9iYWxTaWduIG52LXNhMSEwHwYDVQQDExhHbG9iYWxTaWduIENBIDIgZm9yIEFB +VEwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJS4vhNfaSmXtX7bWy +hVhBGqisbqgnX9/K5psHMrkwm10pnicCSmvb6nvgMAPbRPyfpGHj5ArrDli6rCDR +CLR1tjied/6AxQCCPgyvDyEwDWxzytVHkCldzEHjHmc1kL0zI7aQfNrD25xAUnHa +X2jBm71filgduyQBfuLJLlL/NGh46X6eI9xpqBmqwBFa6wHPisnwkAB22CQB/OY3 +mnlhLCJmrEL9fGPRDQnc6w849ws3nkKISkEPmTyfUAJUkEgCxOjfiwoNSFjZ99lA +K+ujj90dqYU3mJ7dTEHA2+dFrdvVsoyA4JZBu7utxe2rJgpifO3B/L+f4Cat12kA +2IqtAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUxRNOzofGiRsj6EDjdTKbA3A67+8wHwYDVR0jBBgwFoAU +YMLxUj6tjBPc28oO+grmKiyZS9gwgYYGCCsGAQUFBwEBBHoweDA0BggrBgEFBQcw +AYYoaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzYWF0bHNoYTJnMjBABggr +BgEFBQcwAoY0aHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9jYWNlcnQvZ3Nh +YXRsc2hhMmcyLmNydDA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY3JsLmdsb2Jh +bHNpZ24uY29tL2dzL2dzYWF0bHNoYTJnMi5jcmwwRwYDVR0gBEAwPjA8BgRVHSAA +MDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z +aXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAkH9vI0bJPzbtf5ikdFb5Kc4L9/IIZ +GPeVbS3UPl8x4WPzmZHwGvSeG53DDcJ9weGHO4PORBgBT5KDZEt7CKXd4vt83xw8 +P1kYvvjv4E+8R8VD3hP48zkygHMR3JtBdMPkbNbE10TCb4XmQPpkwGIVhaD7ojtS ++4mPjVts6ZZzbnHI42CbYwdaOf2W8IUu0b1w4T7T5YPfi8rSKwQxIKibFG1mSsOC +vG9tDxJVJUNdiWTPoHGn+n+3qeJvPRgHhicZ+ivOqqmLQSNgtp1WdQ+uJmUqU7EY +htN+laG7bS/8xGTPothL9Abgd/9L3X0KKGUDCdcpzRuy20CI7E4uygD8 +-----END CERTIFICATE-----` + +func TestEmbedRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedRevocationStatus(pemToCert(certPem), pemToCert(issuerPem), &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.OCSP) != 1 { + t.Errorf("Expected one OCSP status") + } +} + +func TestEmbedOCSPRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedOCSPRevocationStatus(pemToCert(certPem), pemToCert(issuerPem), &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.OCSP) != 1 { + t.Errorf("Expected one OCSP status") + } +} + +func TestEmbedCRLRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedCRLRevocationStatus(pemToCert(certPem), nil, &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.CRL) != 1 { + t.Errorf("Expected one CRL") + } +} + +func pemToCert(p string) *x509.Certificate { + block, _ := pem.Decode([]byte(p)) + cert, _ := x509.ParseCertificate(block.Bytes) + + return cert +} diff --git a/verify/verify.go b/verify/verify.go index 74b9743..e713c8e 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -11,26 +11,13 @@ import ( "time" "bitbucket.org/digitorus/pdf" + "bitbucket.org/digitorus/pdfsign/revocation" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" - "go/src/log" + "log" "golang.org/x/crypto/ocsp" ) -type RevocationInfoArchival struct { - CRL RevCRL `asn1:"tag:0,optional,explicit"` - OCSP RevOCSP `asn1:"tag:1,optional,explicit"` - OtherRevInfo OtherRevInfo `asn1:"tag:2,optional,explicit"` -} - -type RevCRL []asn1.RawValue -type RevOCSP []asn1.RawValue - -type OtherRevInfo struct { - Type asn1.ObjectIdentifier - Value []byte -} - type Response struct { Error string @@ -192,7 +179,7 @@ func Verify(file *os.File) (apiResp *Response, err error) { } // PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8) - var revInfo RevocationInfoArchival + var revInfo revocation.InfoArchival p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo) // Parse OCSP response